1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright 2019 The Chromium Authors. 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 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process_manager.dart';
import '../convert.dart';
import '../globals.dart';
/// The [ChromeLauncher] instance.
ChromeLauncher get chromeLauncher => context.get<ChromeLauncher>();
/// An environment variable used to override the location of chrome.
const String kChromeEnvironment = 'CHROME_EXECUTABLE';
/// The expected executable name on linux.
const String kLinuxExecutable = 'google-chrome';
/// The expected executable name on macOS.
const String kMacOSExecutable =
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
/// The expected executable name on Windows.
const String kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
/// The possible locations where the chrome executable can be located on windows.
final List<String> kWindowsPrefixes = <String>[
platform.environment['LOCALAPPDATA'],
platform.environment['PROGRAMFILES'],
platform.environment['PROGRAMFILES(X86)']
];
/// Find the chrome executable on the current platform.
///
/// Does not verify whether the executable exists.
String findChromeExecutable() {
if (platform.environment.containsKey(kChromeEnvironment)) {
return platform.environment[kChromeEnvironment];
}
if (platform.isLinux) {
return kLinuxExecutable;
}
if (platform.isMacOS) {
return kMacOSExecutable;
}
if (platform.isWindows) {
final String windowsPrefix = kWindowsPrefixes.firstWhere((String prefix) {
if (prefix == null) {
return false;
}
final String path = fs.path.join(prefix, kWindowsExecutable);
return fs.file(path).existsSync();
}, orElse: () => '.');
return fs.path.join(windowsPrefix, kWindowsExecutable);
}
throwToolExit('Platform ${platform.operatingSystem} is not supported.');
return null;
}
/// Responsible for launching chrome with devtools configured.
class ChromeLauncher {
const ChromeLauncher();
static final Completer<Chrome> _currentCompleter = Completer<Chrome>();
/// Launch the chrome browser to a particular `host` page.
///
/// `headless` defaults to false, and controls whether we open a headless or
/// a `headfull` browser.
Future<Chrome> launch(String url, { bool headless = false }) async {
final String chromeExecutable = findChromeExecutable();
final Directory dataDir = fs.systemTempDirectory.createTempSync();
final int port = await os.findFreePort();
final List<String> args = <String>[
chromeExecutable,
// 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)
...<String>['--headless', '--disable-gpu', '--no-sandbox'],
url,
];
final Process process = await processManager.start(args, runInShell: true);
// Wait until the DevTools are listening before trying to connect.
await process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.firstWhere((String line) => line.startsWith('DevTools listening'), orElse: () {
return 'Failed to spawn stderr';
})
.timeout(const Duration(seconds: 60), onTimeout: () {
throwToolExit('Unable to connect to Chrome DevTools.');
return null;
});
final Uri remoteDebuggerUri = await _getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'));
return _connect(Chrome._(
port,
ChromeConnection('localhost', port),
process: process,
dataDir: dataDir,
remoteDebuggerUri: remoteDebuggerUri,
));
}
static Future<Chrome> _connect(Chrome chrome) async {
if (_currentCompleter.isCompleted) {
throwToolExit('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();
throwToolExit(
'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e');
}
_currentCompleter.complete(chrome);
return chrome;
}
/// 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;
/// Returns the full URL of the Chrome remote debugger for the main page.
///
/// This takes the [base] remote debugger URL (which points to a browser-wide
/// page) and uses its JSON API to find the resolved URL for debugging the host
/// page.
Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
try {
final HttpClient client = HttpClient();
final HttpClientRequest request = await client.getUrl(base.resolve('/json/list'));
final HttpClientResponse response = await request.close();
final List<dynamic> jsonObject = await json.fuse(utf8).decoder.bind(response).single;
return base.resolve(jsonObject.first['devtoolsFrontendUrl']);
} catch (_) {
// If we fail to talk to the remote debugger protocol, give up and return
// the raw URL rather than crashing.
return base;
}
}
}
/// A class for managing an instance of Chrome.
class Chrome {
Chrome._(
this.debugPort,
this.chromeConnection, {
Process process,
Directory dataDir,
this.remoteDebuggerUri,
}) : _process = process,
_dataDir = dataDir;
final int debugPort;
final Process _process;
final Directory _dataDir;
final ChromeConnection chromeConnection;
final Uri remoteDebuggerUri;
static Completer<Chrome> _currentCompleter = Completer<Chrome>();
Future<void> get onExit => _currentCompleter.future;
Future<void> close() async {
if (_currentCompleter.isCompleted) {
_currentCompleter = Completer<Chrome>();
}
chromeConnection.close();
_process?.kill();
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<void>.delayed(const Duration(milliseconds: 500));
} catch (_) {
// Silently fail if we can't clean up the profile information.
} finally {
try {
await _dataDir?.delete(recursive: true);
} on FileSystemException {
printError('failed to delete temporary profile at ${_dataDir.path}');
}
}
}
}