Skip to content

Commit 6381f97

Browse files
author
Anna Gringauze
authored
Webdev serve: add an option to pass user data directory to chrome (#1491)
* Webdev serve: add an option to pass user data directory to chrome Webdev launches chrome using temp profile by default, so the users cannot use their extensions while debugging. Allow launching chrome in a window that is signed in as the user's default profile. Notes: There seems to be a limitation in chrome that only uses the existing session with --user-data-dir. The existing session cannot open a debug port that dwds needs. To work around this issue, we copy the default profile to a temp directory, and pass it to chrome. That seems to work, but takes time to copy the profile. Issue a performance warning in that case. Note that subsequent updates will only update modified files, so the performance hit becomes smaller over time. Closes: #1490 * Updated changelog and test * Fix some test failures * Fixed windows test faiures * Fix test faillures on linux * Fixed format * Fix failing tests on Windows * Add automadic detection of Chrome user data directory * Fixed failing test on Windows * Extend timeout for chrome test to prevent flakes * Added links to issue for supporting the feature on windows
1 parent 079a52e commit 6381f97

File tree

11 files changed

+625
-34
lines changed

11 files changed

+625
-34
lines changed

webdev/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## 2.7.9-dev
2+
3+
- Add an option to pass user data directory to chrome: `user-data-dir`.
4+
Auto detect user data directory based on the current OS if `auto` is
5+
given as a value. If `null` is given as a value (default), fall back
6+
to the existing behavior (i.e. creating/reusing a temp directory).
7+
8+
Note: not supported for Windows yet due to flakiness it introduces.
9+
10+
Example using user-specified directory:
11+
```
12+
webdev serve \
13+
--debug --debug-extension \
14+
--user-data-dir='/Users/<user>/Library/Application Support/Google/Chrome'
15+
```
16+
Example using auto-detected directory:
17+
```
18+
webdev serve \
19+
--debug --debug-extension \
20+
--user-data-dir=auto
21+
```
22+
123
## 2.7.8
224

325
- Update `vm_service` to `^8.1.0`.

webdev/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ Advanced:
7272
specify a specific port to attach to
7373
an already running chrome instance
7474
instead.
75+
--user-data-dir Use with launch-in-chrome to specify
76+
user data directory to pass to
77+
chrome. Will start chrome window
78+
logged into default profile with
79+
enabled extensions. Use `auto` as a
80+
value to infer the default directory
81+
for the current OS. Note: only
82+
supported for Mac OS X and linux
83+
platforms.
7584
--log-requests Enables logging for each request to
7685
the server.
7786
--tls-cert-chain The file location to a TLS

webdev/lib/src/command/configuration.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const hotReloadFlag = 'hot-reload';
2323
const hotRestartFlag = 'hot-restart';
2424
const launchAppOption = 'launch-app';
2525
const launchInChromeFlag = 'launch-in-chrome';
26+
const userDataDirFlag = 'user-data-dir';
2627
const liveReloadFlag = 'live-reload';
2728
const logRequestsFlag = 'log-requests';
2829
const outputFlag = 'output';
@@ -92,6 +93,7 @@ class Configuration {
9293
final String _tlsCertKey;
9394
final List<String> _launchApps;
9495
final bool _launchInChrome;
96+
final String _userDataDir;
9597
final bool _logRequests;
9698
final String _output;
9799
final String outputInput;
@@ -115,6 +117,7 @@ class Configuration {
115117
String tlsCertKey,
116118
List<String> launchApps,
117119
bool launchInChrome,
120+
String userDataDir,
118121
bool logRequests,
119122
String output,
120123
this.outputInput,
@@ -136,6 +139,7 @@ class Configuration {
136139
_tlsCertKey = tlsCertKey,
137140
_launchApps = launchApps,
138141
_launchInChrome = launchInChrome,
142+
_userDataDir = userDataDir,
139143
_logRequests = logRequests,
140144
_output = output,
141145
_release = release,
@@ -185,6 +189,11 @@ class Configuration {
185189
throw InvalidConfiguration(
186190
'--$launchAppOption can only be used with --$launchInChromeFlag');
187191
}
192+
193+
if (userDataDir != null && !launchInChrome) {
194+
throw InvalidConfiguration(
195+
'--$userDataDir can only be used with --$launchInChromeFlag');
196+
}
188197
}
189198

190199
/// Creates a new [Configuration] with all non-null fields from
@@ -201,6 +210,7 @@ class Configuration {
201210
tlsCertKey: other._tlsCertKey ?? _tlsCertKey,
202211
launchApps: other._launchApps ?? _launchApps,
203212
launchInChrome: other._launchInChrome ?? _launchInChrome,
213+
userDataDir: other._userDataDir ?? _userDataDir,
204214
logRequests: other._logRequests ?? _logRequests,
205215
output: other._output ?? _output,
206216
release: other._release ?? _release,
@@ -239,6 +249,8 @@ class Configuration {
239249

240250
bool get launchInChrome => _launchInChrome ?? false;
241251

252+
String get userDataDir => _userDataDir;
253+
242254
bool get logRequests => _logRequests ?? false;
243255

244256
String get output => _output ?? outputNone;
@@ -320,6 +332,10 @@ class Configuration {
320332
? true
321333
: defaultConfiguration.launchInChrome;
322334

335+
var userDataDir = argResults.options.contains(userDataDirFlag)
336+
? argResults[userDataDirFlag] as String
337+
: defaultConfiguration.userDataDir;
338+
323339
var logRequests = argResults.options.contains(logRequestsFlag)
324340
? argResults[logRequestsFlag] as bool
325341
: defaultConfiguration.logRequests;
@@ -382,6 +398,7 @@ class Configuration {
382398
tlsCertKey: tlsCertKey,
383399
launchApps: launchApps,
384400
launchInChrome: launchInChrome,
401+
userDataDir: userDataDir,
385402
logRequests: logRequests,
386403
output: output,
387404
outputInput: outputInput,

webdev/lib/src/command/serve_command.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ refresh: Performs a full page refresh.
5959
help: 'Automatically launches your application in Chrome with the '
6060
'debug port open. Use $chromeDebugPortFlag to specify a specific '
6161
'port to attach to an already running chrome instance instead.')
62+
..addOption(userDataDirFlag,
63+
defaultsTo: null,
64+
help: 'Use with $launchInChromeFlag to specify user data directory '
65+
'to pass to chrome. Will start chrome window logged into default '
66+
'profile with enabled extensions. Use `auto` as a value to infer '
67+
'the default directory for the current OS. '
68+
'Note: only supported for Mac OS X and linux platforms.')
6269
..addFlag(liveReloadFlag,
6370
negatable: false,
6471
help: 'Automatically refreshes the page after each successful build.\n'

webdev/lib/src/serve/chrome.dart

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import 'dart:async';
88
import 'dart:io';
99

1010
import 'package:browser_launcher/browser_launcher.dart' as browser_launcher;
11+
import 'package:logging/logging.dart';
1112
import 'package:path/path.dart' as path;
1213
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
1314

15+
import '../command/configuration.dart';
16+
import 'utils.dart';
17+
18+
var _logger = Logger('ChromeLauncher');
1419
var _currentCompleter = Completer<Chrome>();
1520

1621
/// A class for managing an instance of Chrome.
@@ -38,17 +43,61 @@ class Chrome {
3843
/// Starts Chrome with the remote debug port enabled.
3944
///
4045
/// Each url in [urls] will be loaded in a separate tab.
41-
static Future<Chrome> start(List<String> urls, {int port}) async {
46+
/// Enables chrome devtools with port [port] if specified.
47+
/// Uses a copy of [userDataDir] to sign into the default
48+
/// user profile, or starts a new session without sign in,
49+
/// if not specified.
50+
static Future<Chrome> start(List<String> urls,
51+
{int port, String userDataDir}) async {
52+
var signIn = false;
4253
String dir;
4354
// Re-using the directory causes flakiness on Windows, so on that platform
4455
// pass null to have it create a directory.
56+
// Issue: https://github.com/dart-lang/webdev/issues/1545
4557
if (!Platform.isWindows) {
46-
dir = path.join(Directory.current.absolute.path, '.dart_tool', 'webdev',
47-
'chrome_user_data');
48-
Directory(dir).createSync(recursive: true);
58+
var userDataTemp = path.join(Directory.current.absolute.path,
59+
'.dart_tool', 'webdev', 'chrome_user_data');
60+
var userDataCopy = path.join(Directory.current.absolute.path,
61+
'.dart_tool', 'webdev', 'chrome_user_data_copy');
62+
63+
if (userDataDir != null) {
64+
signIn = true;
65+
dir = userDataCopy;
66+
var stopwatch = Stopwatch()..start();
67+
try {
68+
_logger.info('Copying user data directory...');
69+
_logger.warning(
70+
'Copying user data directory might take >12s on the first '
71+
'use of --$userDataDirFlag, and ~2-3s on subsequent runs. '
72+
'Run without --$userDataDirFlag to improve performance.');
73+
74+
Directory(dir).createSync(recursive: true);
75+
await updatePath(
76+
path.join(userDataDir, 'Default'), path.join(dir, 'Default'));
77+
78+
_logger.info(
79+
'Copied user data directory in ${stopwatch.elapsedMilliseconds} ms');
80+
} catch (e, s) {
81+
dir = userDataTemp;
82+
signIn = false;
83+
if (Directory(dir).existsSync()) {
84+
Directory(dir).deleteSync(recursive: true);
85+
}
86+
_logger.severe('Failed to copy user data directory', e, s);
87+
_logger.severe('Launching with temp profile instead.');
88+
rethrow;
89+
}
90+
}
91+
92+
if (!signIn) {
93+
dir = userDataTemp;
94+
Directory(dir).createSync(recursive: true);
95+
}
4996
}
97+
98+
_logger.info('Starting chrome with user data directory: $dir');
5099
var chrome = await browser_launcher.Chrome.startWithDebugPort(urls,
51-
debugPort: port, userDataDir: dir);
100+
debugPort: port, userDataDir: dir, signIn: signIn);
52101
return _connect(Chrome._(chrome.debugPort, chrome));
53102
}
54103

@@ -79,3 +128,29 @@ class ChromeError extends Error {
79128
return 'ChromeError: $details';
80129
}
81130
}
131+
132+
String autoDetectChromeUserDataDirectory() {
133+
Directory directory;
134+
if (Platform.isMacOS) {
135+
var home = Platform.environment['HOME'];
136+
directory = Directory(
137+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'));
138+
} else if (Platform.isLinux) {
139+
var home = Platform.environment['HOME'];
140+
directory = Directory(path.join(home, '.config', 'google-chrome'));
141+
} else {
142+
_logger.warning('Auto detecting chrome user data directory option is not '
143+
'supported for ${Platform.operatingSystem}');
144+
return null;
145+
}
146+
147+
if (directory.existsSync()) {
148+
_logger.info('Auto detected chrome user data directory: ${directory.path}');
149+
return directory.path;
150+
}
151+
152+
_logger.warning('Cannot automatically detect chrome user data directory. '
153+
'Directory does not exist: ${directory.path}');
154+
155+
return null;
156+
}

webdev/lib/src/serve/dev_workflow.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ Future<Chrome> _startChrome(
7272
];
7373
try {
7474
if (configuration.launchInChrome) {
75-
return await Chrome.start(uris, port: configuration.chromeDebugPort);
75+
var userDataDir = configuration.userDataDir == autoOption
76+
? autoDetectChromeUserDataDirectory()
77+
: configuration.userDataDir;
78+
return await Chrome.start(uris,
79+
port: configuration.chromeDebugPort, userDataDir: userDataDir);
7680
} else if (configuration.chromeDebugPort != 0) {
7781
return await Chrome.fromExisting(configuration.chromeDebugPort);
7882
}

webdev/lib/src/serve/utils.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import 'dart:async';
88
import 'dart:io';
99

10+
import 'package:path/path.dart' as p;
11+
1012
/// Returns a port that is probably, but not definitely, not in use.
1113
///
1214
/// This has a built-in race condition: another process may bind this port at
@@ -23,3 +25,62 @@ Future<int> findUnusedPort() async {
2325
await socket.close();
2426
return port;
2527
}
28+
29+
/// Copy directory [from] to [to].
30+
///
31+
/// Updates contents of [to] if already exists.
32+
Future<void> updatePath(String from, String to) async {
33+
await _removeDeleted(from, to);
34+
await _copyUpdated(from, to);
35+
}
36+
37+
// Update modified files.
38+
Future<void> _copyUpdated(String from, String to) async {
39+
if (!Directory(from).existsSync()) return;
40+
await Directory(to).create(recursive: true);
41+
42+
await for (final file in Directory(from).list()) {
43+
final copyTo = p.join(to, p.relative(file.path, from: from));
44+
if (file is Directory) {
45+
await _copyUpdated(file.path, copyTo);
46+
} else if (file is File) {
47+
var copyToFile = File(copyTo);
48+
if (!copyToFile.existsSync() ||
49+
copyToFile.statSync().modified.compareTo(file.statSync().modified) <
50+
0) {
51+
await File(file.path).copy(copyTo);
52+
}
53+
} else if (file is Link) {
54+
await Link(copyTo).create(await file.target(), recursive: true);
55+
}
56+
}
57+
}
58+
59+
// Remove deleted files.
60+
Future<void> _removeDeleted(String from, String to) async {
61+
if (!Directory(from).existsSync()) {
62+
if (Directory(to).existsSync()) {
63+
await Directory(to).delete(recursive: true);
64+
}
65+
return;
66+
}
67+
68+
if (!Directory(to).existsSync()) return;
69+
await for (final file in Directory(to).list()) {
70+
final copyFrom = p.join(from, p.relative(file.path, from: to));
71+
if (file is File) {
72+
var copyFromFile = File(copyFrom);
73+
if (!copyFromFile.existsSync()) {
74+
await File(file.path).delete();
75+
}
76+
} else if (file is Directory) {
77+
var copyFromDir = Directory(copyFrom);
78+
await _removeDeleted(copyFromDir.path, file.path);
79+
} else if (file is Link) {
80+
var copyFromDir = Link(copyFrom);
81+
if (!copyFromDir.existsSync()) {
82+
await Link(file.path).delete();
83+
}
84+
}
85+
}
86+
}

webdev/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies:
1515
args: ^2.0.0
1616
async: ^2.2.0
1717
build_daemon: '>=2.0.0 <4.0.0'
18-
browser_launcher: ^1.0.0
18+
browser_launcher: ^1.1.0
1919
crypto: ^3.0.0
2020
dds: ^2.2.0
2121
dwds: ^12.0.0

0 commit comments

Comments
 (0)