Skip to content

Webdev serve: add an option to pass user data directory to chrome #1491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions webdev/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## 2.7.9-dev

- Add an option to pass user data directory to chrome: `user-data-dir`.
Auto detect user data directory based on the current OS if `auto` is
given as a value. If `null` is given as a value (default), fall back
to the existing behavior (i.e. creating/reusing a temp directory).

Note: not supported for Windows yet due to flakiness it introduces.

Example using user-specified directory:
```
webdev serve \
--debug --debug-extension \
--user-data-dir='/Users/<user>/Library/Application Support/Google/Chrome'
```
Example using auto-detected directory:
```
webdev serve \
--debug --debug-extension \
--user-data-dir=auto
```

## 2.7.8

- Update `vm_service` to `^8.1.0`.
Expand Down
9 changes: 9 additions & 0 deletions webdev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ Advanced:
specify a specific port to attach to
an already running chrome instance
instead.
--user-data-dir Use with launch-in-chrome to specify
user data directory to pass to
chrome. Will start chrome window
logged into default profile with
enabled extensions. Use `auto` as a
value to infer the default directory
for the current OS. Note: only
supported for Mac OS X and linux
platforms.
--log-requests Enables logging for each request to
the server.
--tls-cert-chain The file location to a TLS
Expand Down
17 changes: 17 additions & 0 deletions webdev/lib/src/command/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const hotReloadFlag = 'hot-reload';
const hotRestartFlag = 'hot-restart';
const launchAppOption = 'launch-app';
const launchInChromeFlag = 'launch-in-chrome';
const userDataDirFlag = 'user-data-dir';
const liveReloadFlag = 'live-reload';
const logRequestsFlag = 'log-requests';
const outputFlag = 'output';
Expand Down Expand Up @@ -92,6 +93,7 @@ class Configuration {
final String _tlsCertKey;
final List<String> _launchApps;
final bool _launchInChrome;
final String _userDataDir;
final bool _logRequests;
final String _output;
final String outputInput;
Expand All @@ -115,6 +117,7 @@ class Configuration {
String tlsCertKey,
List<String> launchApps,
bool launchInChrome,
String userDataDir,
bool logRequests,
String output,
this.outputInput,
Expand All @@ -136,6 +139,7 @@ class Configuration {
_tlsCertKey = tlsCertKey,
_launchApps = launchApps,
_launchInChrome = launchInChrome,
_userDataDir = userDataDir,
_logRequests = logRequests,
_output = output,
_release = release,
Expand Down Expand Up @@ -185,6 +189,11 @@ class Configuration {
throw InvalidConfiguration(
'--$launchAppOption can only be used with --$launchInChromeFlag');
}

if (userDataDir != null && !launchInChrome) {
throw InvalidConfiguration(
'--$userDataDir can only be used with --$launchInChromeFlag');
}
}

/// Creates a new [Configuration] with all non-null fields from
Expand All @@ -201,6 +210,7 @@ class Configuration {
tlsCertKey: other._tlsCertKey ?? _tlsCertKey,
launchApps: other._launchApps ?? _launchApps,
launchInChrome: other._launchInChrome ?? _launchInChrome,
userDataDir: other._userDataDir ?? _userDataDir,
logRequests: other._logRequests ?? _logRequests,
output: other._output ?? _output,
release: other._release ?? _release,
Expand Down Expand Up @@ -239,6 +249,8 @@ class Configuration {

bool get launchInChrome => _launchInChrome ?? false;

String get userDataDir => _userDataDir;

bool get logRequests => _logRequests ?? false;

String get output => _output ?? outputNone;
Expand Down Expand Up @@ -320,6 +332,10 @@ class Configuration {
? true
: defaultConfiguration.launchInChrome;

var userDataDir = argResults.options.contains(userDataDirFlag)
? argResults[userDataDirFlag] as String
: defaultConfiguration.userDataDir;

var logRequests = argResults.options.contains(logRequestsFlag)
? argResults[logRequestsFlag] as bool
: defaultConfiguration.logRequests;
Expand Down Expand Up @@ -382,6 +398,7 @@ class Configuration {
tlsCertKey: tlsCertKey,
launchApps: launchApps,
launchInChrome: launchInChrome,
userDataDir: userDataDir,
logRequests: logRequests,
output: output,
outputInput: outputInput,
Expand Down
7 changes: 7 additions & 0 deletions webdev/lib/src/command/serve_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ refresh: Performs a full page refresh.
help: 'Automatically launches your application in Chrome with the '
'debug port open. Use $chromeDebugPortFlag to specify a specific '
'port to attach to an already running chrome instance instead.')
..addOption(userDataDirFlag,
defaultsTo: null,
help: 'Use with $launchInChromeFlag to specify user data directory '
'to pass to chrome. Will start chrome window logged into default '
'profile with enabled extensions. Use `auto` as a value to infer '
'the default directory for the current OS. '
'Note: only supported for Mac OS X and linux platforms.')
..addFlag(liveReloadFlag,
negatable: false,
help: 'Automatically refreshes the page after each successful build.\n'
Expand Down
85 changes: 80 additions & 5 deletions webdev/lib/src/serve/chrome.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import 'dart:async';
import 'dart:io';

import 'package:browser_launcher/browser_launcher.dart' as browser_launcher;
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

import '../command/configuration.dart';
import 'utils.dart';

var _logger = Logger('ChromeLauncher');
var _currentCompleter = Completer<Chrome>();

/// A class for managing an instance of Chrome.
Expand Down Expand Up @@ -38,17 +43,61 @@ class Chrome {
/// Starts Chrome with the remote debug port enabled.
///
/// Each url in [urls] will be loaded in a separate tab.
static Future<Chrome> start(List<String> urls, {int port}) async {
/// Enables chrome devtools with port [port] if specified.
/// Uses a copy of [userDataDir] to sign into the default
/// user profile, or starts a new session without sign in,
/// if not specified.
static Future<Chrome> start(List<String> urls,
{int port, String userDataDir}) async {
var signIn = false;
String dir;
// Re-using the directory causes flakiness on Windows, so on that platform
// pass null to have it create a directory.
// Issue: https://github.com/dart-lang/webdev/issues/1545
if (!Platform.isWindows) {
dir = path.join(Directory.current.absolute.path, '.dart_tool', 'webdev',
'chrome_user_data');
Directory(dir).createSync(recursive: true);
var userDataTemp = path.join(Directory.current.absolute.path,
'.dart_tool', 'webdev', 'chrome_user_data');
var userDataCopy = path.join(Directory.current.absolute.path,
'.dart_tool', 'webdev', 'chrome_user_data_copy');

if (userDataDir != null) {
signIn = true;
dir = userDataCopy;
var stopwatch = Stopwatch()..start();
try {
_logger.info('Copying user data directory...');
_logger.warning(
'Copying user data directory might take >12s on the first '
'use of --$userDataDirFlag, and ~2-3s on subsequent runs. '
'Run without --$userDataDirFlag to improve performance.');

Directory(dir).createSync(recursive: true);
await updatePath(
path.join(userDataDir, 'Default'), path.join(dir, 'Default'));

_logger.info(
'Copied user data directory in ${stopwatch.elapsedMilliseconds} ms');
} catch (e, s) {
dir = userDataTemp;
signIn = false;
if (Directory(dir).existsSync()) {
Directory(dir).deleteSync(recursive: true);
}
_logger.severe('Failed to copy user data directory', e, s);
_logger.severe('Launching with temp profile instead.');
rethrow;
}
}

if (!signIn) {
dir = userDataTemp;
Directory(dir).createSync(recursive: true);
}
}

_logger.info('Starting chrome with user data directory: $dir');
var chrome = await browser_launcher.Chrome.startWithDebugPort(urls,
debugPort: port, userDataDir: dir);
debugPort: port, userDataDir: dir, signIn: signIn);
return _connect(Chrome._(chrome.debugPort, chrome));
}

Expand Down Expand Up @@ -79,3 +128,29 @@ class ChromeError extends Error {
return 'ChromeError: $details';
}
}

String autoDetectChromeUserDataDirectory() {
Directory directory;
if (Platform.isMacOS) {
var home = Platform.environment['HOME'];
directory = Directory(
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'));
} else if (Platform.isLinux) {
var home = Platform.environment['HOME'];
directory = Directory(path.join(home, '.config', 'google-chrome'));
} else {
_logger.warning('Auto detecting chrome user data directory option is not '
'supported for ${Platform.operatingSystem}');
return null;
}

if (directory.existsSync()) {
_logger.info('Auto detected chrome user data directory: ${directory.path}');
return directory.path;
}

_logger.warning('Cannot automatically detect chrome user data directory. '
'Directory does not exist: ${directory.path}');

return null;
}
6 changes: 5 additions & 1 deletion webdev/lib/src/serve/dev_workflow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ Future<Chrome> _startChrome(
];
try {
if (configuration.launchInChrome) {
return await Chrome.start(uris, port: configuration.chromeDebugPort);
var userDataDir = configuration.userDataDir == autoOption
? autoDetectChromeUserDataDirectory()
: configuration.userDataDir;
return await Chrome.start(uris,
port: configuration.chromeDebugPort, userDataDir: userDataDir);
} else if (configuration.chromeDebugPort != 0) {
return await Chrome.fromExisting(configuration.chromeDebugPort);
}
Expand Down
61 changes: 61 additions & 0 deletions webdev/lib/src/serve/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import 'dart:async';
import 'dart:io';

import 'package:path/path.dart' as p;

/// 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
Expand All @@ -23,3 +25,62 @@ Future<int> findUnusedPort() async {
await socket.close();
return port;
}

/// Copy directory [from] to [to].
///
/// Updates contents of [to] if already exists.
Future<void> updatePath(String from, String to) async {
await _removeDeleted(from, to);
await _copyUpdated(from, to);
}

// Update modified files.
Future<void> _copyUpdated(String from, String to) async {
if (!Directory(from).existsSync()) return;
await Directory(to).create(recursive: true);

await for (final file in Directory(from).list()) {
final copyTo = p.join(to, p.relative(file.path, from: from));
if (file is Directory) {
await _copyUpdated(file.path, copyTo);
} else if (file is File) {
var copyToFile = File(copyTo);
if (!copyToFile.existsSync() ||
copyToFile.statSync().modified.compareTo(file.statSync().modified) <
0) {
await File(file.path).copy(copyTo);
}
} else if (file is Link) {
await Link(copyTo).create(await file.target(), recursive: true);
}
}
}

// Remove deleted files.
Future<void> _removeDeleted(String from, String to) async {
if (!Directory(from).existsSync()) {
if (Directory(to).existsSync()) {
await Directory(to).delete(recursive: true);
}
return;
}

if (!Directory(to).existsSync()) return;
await for (final file in Directory(to).list()) {
final copyFrom = p.join(from, p.relative(file.path, from: to));
if (file is File) {
var copyFromFile = File(copyFrom);
if (!copyFromFile.existsSync()) {
await File(file.path).delete();
}
} else if (file is Directory) {
var copyFromDir = Directory(copyFrom);
await _removeDeleted(copyFromDir.path, file.path);
} else if (file is Link) {
var copyFromDir = Link(copyFrom);
if (!copyFromDir.existsSync()) {
await Link(file.path).delete();
}
}
}
}
2 changes: 1 addition & 1 deletion webdev/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies:
args: ^2.0.0
async: ^2.2.0
build_daemon: '>=2.0.0 <4.0.0'
browser_launcher: ^1.0.0
browser_launcher: ^1.1.0
crypto: ^3.0.0
dds: ^2.2.0
dwds: ^12.0.0
Expand Down
Loading