07680e9603
Required to fix https://github.com/flutter/devtools/issues/9176. Change-Id: I6648a797a6ff4c22073ab49af5d584d2df52036a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/429064 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Kenzie Davisson <kenzieschmoll@google.com>
823 lines
25 KiB
Dart
823 lines
25 KiB
Dart
// Copyright (c) 2022, 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:args/args.dart';
|
|
import 'package:browser_launcher/browser_launcher.dart';
|
|
import 'package:devtools_shared/devtools_extensions_io.dart';
|
|
import 'package:devtools_shared/devtools_shared.dart';
|
|
import 'package:http_multi_server/http_multi_server.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:shelf/shelf.dart' as shelf;
|
|
import 'package:shelf/shelf_io.dart' as shelf;
|
|
|
|
import 'src/devtools/client.dart';
|
|
import 'src/devtools/dtd.dart';
|
|
import 'src/devtools/handler.dart';
|
|
import 'src/devtools/machine_mode_command_handler.dart';
|
|
import 'src/devtools/memory_profile.dart';
|
|
import 'src/devtools/utils.dart';
|
|
import 'src/utils/console.dart';
|
|
|
|
class DevToolsServer {
|
|
static const protocolVersion = '1.2.0';
|
|
static const defaultTryPorts = 10;
|
|
static const defaultDdsHost = '127.0.0.1';
|
|
static const defaultDdsPort = 0;
|
|
static const commandDescription =
|
|
'Open DevTools (optionally connecting to an existing application).';
|
|
|
|
static const argHelp = 'help';
|
|
static const argVmUri = 'vm-uri';
|
|
static const argEnableNotifications = 'enable-notifications';
|
|
static const argAllowEmbedding = 'allow-embedding';
|
|
static const argAppSizeBase = 'app-size-base';
|
|
static const argAppSizeTest = 'app-size-test';
|
|
static const argHeadlessMode = 'headless';
|
|
static const argDdsHost = 'dds-host';
|
|
static const argDdsPort = 'dds-port';
|
|
static const argDebugMode = 'debug';
|
|
static const argDisableCors = 'disable-cors';
|
|
static const argDtdUri = 'dtd-uri';
|
|
static const argDtdExposedUri = 'dtd-exposed-uri';
|
|
static const argPrintDtd = 'print-dtd';
|
|
static const argLaunchBrowser = 'launch-browser';
|
|
static const argMachine = 'machine';
|
|
static const argHost = 'host';
|
|
static const argPort = 'port';
|
|
static const argProfileMemory = 'record-memory-profile';
|
|
static const argTryPorts = 'try-ports';
|
|
static const argVerbose = 'verbose';
|
|
static const argVersion = 'version';
|
|
static const launchDevToolsService = 'launchDevTools';
|
|
|
|
MachineModeCommandHandler? _machineModeCommandHandler;
|
|
late ClientManager clientManager;
|
|
final bool _isChromeOS = File('/dev/.cros_milestone').existsSync();
|
|
|
|
/// Builds an arg parser for the DevTools server.
|
|
///
|
|
/// [includeHelpOption] should be set to false if this arg parser will be used
|
|
/// in a Command subclass.
|
|
static ArgParser buildArgParser({
|
|
bool verbose = false,
|
|
bool includeHelpOption = true,
|
|
int? usageLineLength,
|
|
}) {
|
|
final argParser = ArgParser(
|
|
usageLineLength: usageLineLength,
|
|
);
|
|
|
|
if (includeHelpOption) {
|
|
argParser.addFlag(
|
|
argHelp,
|
|
negatable: false,
|
|
abbr: 'h',
|
|
help: 'Prints help output.',
|
|
);
|
|
}
|
|
argParser
|
|
..addFlag(
|
|
argVersion,
|
|
negatable: false,
|
|
help: 'Prints the DevTools version.',
|
|
)
|
|
..addFlag(
|
|
argVerbose,
|
|
negatable: false,
|
|
abbr: 'v',
|
|
help: 'Output more informational messages.',
|
|
)
|
|
..addOption(
|
|
argHost,
|
|
valueHelp: 'host',
|
|
help: 'Hostname to serve DevTools on (defaults to localhost).',
|
|
)
|
|
..addOption(
|
|
argPort,
|
|
defaultsTo: '9100',
|
|
valueHelp: 'port',
|
|
help: 'Port to serve DevTools on; specify 0 to automatically use any '
|
|
'available port.',
|
|
)
|
|
..addOption(
|
|
argDtdUri,
|
|
valueHelp: 'uri',
|
|
help: 'A URI pointing to a Dart Tooling Daemon that DevTools should '
|
|
'interface with.',
|
|
)
|
|
..addOption(
|
|
argDtdExposedUri,
|
|
valueHelp: 'uri',
|
|
help: 'An optional URI for the DartTooling Daemon (--dtd-uri) that has '
|
|
'been exposed to the front-end to support environments split across '
|
|
'machines such as a web-based editor.',
|
|
)
|
|
..addFlag(
|
|
argLaunchBrowser,
|
|
help:
|
|
'Launches DevTools in a browser immediately at start.\n(defaults to on unless in --machine mode)',
|
|
)
|
|
..addFlag(
|
|
argMachine,
|
|
negatable: false,
|
|
help: 'Sets output format to JSON for consumption in tools.',
|
|
)
|
|
..addSeparator('Memory profiling options:')
|
|
..addOption(
|
|
argProfileMemory,
|
|
valueHelp: 'file',
|
|
defaultsTo: 'memory_samples.json',
|
|
help:
|
|
'Start devtools headlessly and write memory profiling samples to the '
|
|
'indicated file.',
|
|
);
|
|
|
|
argParser.addSeparator('App size options:');
|
|
|
|
argParser
|
|
..addOption(
|
|
argAppSizeBase,
|
|
aliases: ['appSizeBase'],
|
|
valueHelp: 'file',
|
|
help: 'Path to the base app size file used for app size debugging.',
|
|
)
|
|
..addOption(
|
|
argAppSizeTest,
|
|
aliases: ['appSizeTest'],
|
|
valueHelp: 'file',
|
|
help:
|
|
'Path to the test app size file used for app size debugging.\nThis '
|
|
'file should only be specified if --$argAppSizeBase is also '
|
|
'specified.',
|
|
hide: !verbose,
|
|
);
|
|
|
|
if (verbose) {
|
|
argParser.addSeparator('Advanced options:');
|
|
}
|
|
|
|
// Args to show for verbose mode.
|
|
argParser
|
|
..addOption(
|
|
argTryPorts,
|
|
defaultsTo: DevToolsServer.defaultTryPorts.toString(),
|
|
valueHelp: 'count',
|
|
help: 'The number of ascending ports to try binding to before failing '
|
|
'with an error.',
|
|
hide: !verbose,
|
|
)
|
|
..addOption(
|
|
argDdsHost,
|
|
defaultsTo: DevToolsServer.defaultDdsHost,
|
|
valueHelp: 'bind-address',
|
|
help:
|
|
"The address the Dart Development Service (DDS) should attempt to "
|
|
"bind to if a DDS instance isn't active and a VM service URI is "
|
|
"provided.",
|
|
hide: !verbose,
|
|
)
|
|
..addOption(
|
|
argDdsPort,
|
|
defaultsTo: DevToolsServer.defaultDdsPort.toString(),
|
|
valueHelp: 'port',
|
|
help:
|
|
"The address the Dart Development Service (DDS) should attempt to "
|
|
"bind to if a DDS instance isn't active and a VM service URI is "
|
|
"provided.",
|
|
hide: !verbose,
|
|
)
|
|
..addFlag(
|
|
argEnableNotifications,
|
|
negatable: false,
|
|
help: 'Requests notification permissions immediately when a client '
|
|
'connects back to the server.',
|
|
hide: !verbose,
|
|
)
|
|
..addFlag(
|
|
argAllowEmbedding,
|
|
help: 'Allow embedding DevTools inside an iframe.',
|
|
hide: !verbose,
|
|
)
|
|
..addFlag(
|
|
argDisableCors,
|
|
help:
|
|
'Disable CORS so that the DevTools server can communicate with the '
|
|
'DevTools front end served on a different origin. Use caution when '
|
|
'passing this flag since allowing access to the DevTools server '
|
|
'from other origins may have security implications.',
|
|
defaultsTo: false,
|
|
hide: verbose,
|
|
)
|
|
..addFlag(
|
|
argHeadlessMode,
|
|
negatable: false,
|
|
help: 'Causes the server to spawn Chrome in headless mode for use in '
|
|
'automated testing.',
|
|
hide: !verbose,
|
|
)
|
|
..addFlag(
|
|
argPrintDtd,
|
|
negatable: false,
|
|
help: 'Print the address of the Dart Tooling Daemon, if one is hosted '
|
|
'by the DevTools server.',
|
|
hide: !verbose,
|
|
);
|
|
|
|
// Deprecated and hidden args.
|
|
// TODO: Remove this - prefer that clients use the rest arg.
|
|
argParser
|
|
..addOption(
|
|
argVmUri,
|
|
defaultsTo: '',
|
|
help: 'VM Service protocol URI.',
|
|
hide: true,
|
|
)
|
|
|
|
// Development only args.
|
|
..addFlag(
|
|
argDebugMode,
|
|
negatable: false,
|
|
help: 'Run a debug build of the DevTools web frontend.',
|
|
hide: true,
|
|
);
|
|
|
|
return argParser;
|
|
}
|
|
|
|
/// Serves DevTools.
|
|
///
|
|
/// `handler` is the [shelf.Handler] that the server will use for all requests.
|
|
/// If null, [defaultHandler] will be used. Defaults to null.
|
|
///
|
|
/// `customDevToolsPath` is a path to a directory containing a pre-built
|
|
/// DevTools application.
|
|
///
|
|
// Note: this method is used by the Dart CLI and by package:dwds.
|
|
Future<HttpServer?> serveDevTools({
|
|
bool enableStdinCommands = true,
|
|
bool machineMode = false,
|
|
bool debugMode = false,
|
|
bool launchBrowser = false,
|
|
bool enableNotifications = false,
|
|
bool allowEmbedding = true,
|
|
bool disableCors = false,
|
|
bool headlessMode = false,
|
|
bool verboseMode = false,
|
|
bool printDtdUri = false,
|
|
String? hostname,
|
|
String? customDevToolsPath,
|
|
int port = 0,
|
|
int numPortsToTry = defaultTryPorts,
|
|
shelf.Handler? handler,
|
|
String? serviceProtocolUri,
|
|
String? profileFilename,
|
|
String? appSizeBase,
|
|
String? appSizeTest,
|
|
DtdInfo? dtdInfo,
|
|
}) async {
|
|
hostname ??= 'localhost';
|
|
|
|
// Collect profiling information.
|
|
if (profileFilename != null && serviceProtocolUri != null) {
|
|
final Uri? vmServiceUri = Uri.tryParse(serviceProtocolUri);
|
|
if (vmServiceUri != null) {
|
|
await _hookupMemoryProfiling(
|
|
vmServiceUri,
|
|
profileFilename,
|
|
verboseMode,
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (machineMode) {
|
|
assert(
|
|
enableStdinCommands,
|
|
'machineMode only works with enableStdinCommands.',
|
|
);
|
|
}
|
|
|
|
clientManager = ClientManager(
|
|
requestNotificationPermissions: enableNotifications,
|
|
);
|
|
|
|
dtdInfo ??= await startDtd(
|
|
machineMode: machineMode,
|
|
printDtdUri: printDtdUri,
|
|
);
|
|
|
|
handler ??= await defaultHandler(
|
|
buildDir: customDevToolsPath,
|
|
clientManager: clientManager,
|
|
dtd: dtdInfo,
|
|
devtoolsExtensionsManager: ExtensionsManager(),
|
|
);
|
|
|
|
HttpServer? server;
|
|
SocketException? ex;
|
|
while (server == null && numPortsToTry >= 0) {
|
|
// If we have tried [numPortsToTry] ports and still have not been able to
|
|
// connect, try port 0 to find a random available port.
|
|
if (numPortsToTry == 0) port = 0;
|
|
|
|
try {
|
|
server = await HttpMultiServer.bind(hostname, port);
|
|
} on SocketException catch (e) {
|
|
ex = e;
|
|
numPortsToTry--;
|
|
port++;
|
|
}
|
|
}
|
|
|
|
// Re-throw the last exception if we failed to bind.
|
|
if (server == null && ex != null) {
|
|
throw ex;
|
|
}
|
|
|
|
// Type promote server.
|
|
server!;
|
|
|
|
if (allowEmbedding) {
|
|
server.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
|
|
// The origin-agent-cluster header is required to support the embedding of
|
|
// Dart DevTools in Chrome DevTools.
|
|
server.defaultResponseHeaders.add('origin-agent-cluster', '?1');
|
|
}
|
|
|
|
// CORS restrictions may be disabled for the purposes of debugging or
|
|
// testing when it is useful to allow connecting a debug instance of
|
|
// DevTools app to a running DevTools server.
|
|
if (disableCors) {
|
|
server.defaultResponseHeaders.add(
|
|
HttpHeaders.accessControlAllowOriginHeader,
|
|
'*',
|
|
);
|
|
}
|
|
|
|
// Ensure browsers don't cache older versions of the app.
|
|
server.defaultResponseHeaders.add(
|
|
HttpHeaders.cacheControlHeader,
|
|
'no-store',
|
|
);
|
|
|
|
// Add the headers required to serve with wasm.
|
|
server.defaultResponseHeaders
|
|
..add('Cross-Origin-Embedder-Policy', 'credentialless')
|
|
..add('Cross-Origin-Opener-Policy', 'same-origin')
|
|
..add('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
|
|
// Serve requests in an error zone to prevent failures
|
|
// when running from another error zone.
|
|
runZonedGuarded(
|
|
() => shelf.serveRequests(server!, handler!),
|
|
(e, _) => print('Error serving requests: $e'),
|
|
);
|
|
|
|
final devToolsUrl = 'http://${server.address.host}:${server.port}';
|
|
|
|
if (launchBrowser) {
|
|
if (serviceProtocolUri != null) {
|
|
serviceProtocolUri =
|
|
normalizeVmServiceUri(serviceProtocolUri).toString();
|
|
}
|
|
|
|
final queryParameters = {
|
|
if (serviceProtocolUri != null) 'uri': serviceProtocolUri,
|
|
if (appSizeBase != null) 'appSizeBase': appSizeBase,
|
|
if (appSizeTest != null) 'appSizeTest': appSizeTest,
|
|
};
|
|
String url = Uri.parse(devToolsUrl)
|
|
.replace(queryParameters: queryParameters)
|
|
.toString();
|
|
|
|
// If app size parameters are present, open to the standalone `app-size`
|
|
// page, regardless if there is a vm service uri specified. We only check
|
|
// for the presence of [appSizeBase] here because [appSizeTest] may or may
|
|
// not be specified (it should only be present for diffs). If [appSizeTest]
|
|
// is present without [appSizeBase], we will ignore the parameter.
|
|
if (appSizeBase != null) {
|
|
final startQueryParamIndex = url.indexOf('?');
|
|
if (startQueryParamIndex != -1) {
|
|
url = '${url.substring(0, startQueryParamIndex)}'
|
|
'/#/app-size'
|
|
'${url.substring(startQueryParamIndex)}';
|
|
}
|
|
}
|
|
|
|
try {
|
|
await Chrome.start([url]);
|
|
} catch (e) {
|
|
print('Unable to launch Chrome: $e\n');
|
|
}
|
|
}
|
|
|
|
if (enableStdinCommands) {
|
|
String message = '''Serving DevTools at $devToolsUrl.
|
|
|
|
Hit ctrl-c to terminate the server.''';
|
|
if (!machineMode && debugMode) {
|
|
// Add bold to help find the correct url to open.
|
|
message = ConsoleUtils.bold('$message\n');
|
|
}
|
|
|
|
DevToolsUtils.printOutput(
|
|
message,
|
|
{
|
|
'event': 'server.started',
|
|
// TODO(dantup): Remove this `method` field when we're sure VS Code
|
|
// users are all on a newer version that uses `event`. We incorrectly
|
|
// used `method` for the original releases.
|
|
'method': 'server.started',
|
|
'params': {
|
|
'host': server.address.host,
|
|
'port': server.port,
|
|
'pid': pid,
|
|
'protocolVersion': protocolVersion,
|
|
}
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
|
|
if (machineMode) {
|
|
_machineModeCommandHandler = MachineModeCommandHandler(server: this);
|
|
await _machineModeCommandHandler!.initialize(
|
|
devToolsUrl: devToolsUrl,
|
|
headlessMode: headlessMode,
|
|
);
|
|
}
|
|
}
|
|
|
|
return server;
|
|
}
|
|
|
|
void _printUsage(ArgParser argParser) {
|
|
print(commandDescription);
|
|
print('\nUsage: devtools [arguments] [service protocol uri]');
|
|
print(argParser.usage);
|
|
}
|
|
|
|
/// Wraps [serveDevTools] `arguments` parsed, as from the command line.
|
|
///
|
|
/// For more information on `handler`, see [serveDevTools].
|
|
// Note: this method is used in google3 as well as by DevTools' main method.
|
|
Future<HttpServer?> serveDevToolsWithArgs(
|
|
List<String> arguments, {
|
|
shelf.Handler? handler,
|
|
String? customDevToolsPath,
|
|
}) async {
|
|
ArgResults args;
|
|
final verbose = arguments.contains('-v') || arguments.contains('--verbose');
|
|
final argParser = buildArgParser(verbose: verbose);
|
|
try {
|
|
args = argParser.parse(arguments);
|
|
} on FormatException catch (e) {
|
|
print(e.message);
|
|
print('');
|
|
_printUsage(argParser);
|
|
return null;
|
|
}
|
|
|
|
return await _serveDevToolsWithArgs(
|
|
args,
|
|
verbose,
|
|
handler: handler,
|
|
customDevToolsPath: customDevToolsPath,
|
|
);
|
|
}
|
|
|
|
Future<HttpServer?> _serveDevToolsWithArgs(
|
|
ArgResults args,
|
|
bool verbose, {
|
|
shelf.Handler? handler,
|
|
String? customDevToolsPath,
|
|
}) async {
|
|
final help = args[argHelp];
|
|
final bool version = args[argVersion];
|
|
final bool machineMode = args[argMachine];
|
|
// launchBrowser defaults based on machine-mode if not explicitly supplied.
|
|
final bool launchBrowser = args.wasParsed(argLaunchBrowser)
|
|
? args[argLaunchBrowser]
|
|
: !machineMode;
|
|
final bool enableNotifications = args[argEnableNotifications];
|
|
final bool allowEmbedding =
|
|
args.wasParsed(argAllowEmbedding) ? args[argAllowEmbedding] : true;
|
|
final bool disableCors =
|
|
args.wasParsed(argDisableCors) ? args[argDisableCors] : false;
|
|
|
|
final port = args[argPort] != null ? int.tryParse(args[argPort]) ?? 0 : 0;
|
|
|
|
final bool headlessMode = args[argHeadlessMode];
|
|
final bool debugMode = args[argDebugMode];
|
|
|
|
final numPortsToTry = args[argTryPorts] != null
|
|
? int.tryParse(args[argTryPorts]) ?? 0
|
|
: defaultTryPorts;
|
|
|
|
final bool verboseMode = args[argVerbose];
|
|
final String? hostname = args[argHost];
|
|
|
|
// A helper to print a message and usage information that can be used in
|
|
// a return statement.
|
|
Null printUsage(String message) {
|
|
print(message);
|
|
print('');
|
|
_printUsage(buildArgParser(verbose: verbose));
|
|
}
|
|
|
|
Uri? dtdUri;
|
|
if (args.wasParsed(argDtdUri)) {
|
|
dtdUri = Uri.tryParse(args[argDtdUri]);
|
|
if (dtdUri == null || !dtdUri.hasScheme) {
|
|
return printUsage('--dtd-uri must be a valid URI');
|
|
}
|
|
}
|
|
|
|
Uri? dtdExposedUri;
|
|
if (args.wasParsed(argDtdExposedUri)) {
|
|
if (dtdUri == null) {
|
|
return printUsage(
|
|
'--dtd-exposed-uri can only be supplied with --dtd-uri');
|
|
}
|
|
|
|
dtdExposedUri = Uri.tryParse(args[argDtdExposedUri]);
|
|
if (dtdExposedUri == null || !dtdExposedUri.hasScheme) {
|
|
return printUsage('--dtd-exposed-uri must be a valid URI');
|
|
}
|
|
}
|
|
|
|
final printDtdUri = args.wasParsed(argPrintDtd);
|
|
|
|
if (help) {
|
|
return printUsage(
|
|
'Dart DevTools version ${await DevToolsUtils.getVersion(customDevToolsPath ?? "")}');
|
|
}
|
|
|
|
if (version) {
|
|
final versionStr =
|
|
await DevToolsUtils.getVersion(customDevToolsPath ?? '');
|
|
DevToolsUtils.printOutput(
|
|
'Dart DevTools version $versionStr',
|
|
{
|
|
'version': versionStr,
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// Prefer getting the VM URI from the rest args; fall back on the 'vm-url'
|
|
// option otherwise.
|
|
String? serviceProtocolUri;
|
|
if (args.rest.isNotEmpty) {
|
|
serviceProtocolUri = args.rest.first;
|
|
} else if (args.wasParsed(argVmUri)) {
|
|
serviceProtocolUri = args[argVmUri];
|
|
}
|
|
|
|
// Support collecting profile data.
|
|
String? profileFilename;
|
|
if (args.wasParsed(argProfileMemory)) {
|
|
profileFilename = args[argProfileMemory];
|
|
}
|
|
if (profileFilename != null && !path.isAbsolute(profileFilename)) {
|
|
profileFilename = path.absolute(profileFilename);
|
|
}
|
|
|
|
// App size info.
|
|
String? appSizeBase = args[argAppSizeBase];
|
|
if (appSizeBase != null && !path.isAbsolute(appSizeBase)) {
|
|
appSizeBase = path.absolute(appSizeBase);
|
|
}
|
|
String? appSizeTest = args[argAppSizeTest];
|
|
if (appSizeTest != null && !path.isAbsolute(appSizeTest)) {
|
|
appSizeTest = path.absolute(appSizeTest);
|
|
}
|
|
|
|
return serveDevTools(
|
|
machineMode: machineMode,
|
|
debugMode: debugMode,
|
|
launchBrowser: launchBrowser,
|
|
enableNotifications: enableNotifications,
|
|
allowEmbedding: allowEmbedding,
|
|
disableCors: disableCors,
|
|
port: port,
|
|
headlessMode: headlessMode,
|
|
numPortsToTry: numPortsToTry,
|
|
handler: handler,
|
|
customDevToolsPath: customDevToolsPath,
|
|
serviceProtocolUri: serviceProtocolUri,
|
|
profileFilename: profileFilename,
|
|
verboseMode: verboseMode,
|
|
hostname: hostname,
|
|
appSizeBase: appSizeBase,
|
|
appSizeTest: appSizeTest,
|
|
dtdInfo:
|
|
dtdUri != null ? DtdInfo(dtdUri, exposedUri: dtdExposedUri) : null,
|
|
printDtdUri: printDtdUri,
|
|
);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> launchDevTools(
|
|
Map<String, dynamic> params,
|
|
Uri vmServiceUri,
|
|
String devToolsUrl,
|
|
bool headlessMode,
|
|
bool machineMode,
|
|
) async {
|
|
// First see if we have an existing DevTools client open that we can
|
|
// reuse.
|
|
final canReuse =
|
|
params.containsKey('reuseWindows') && params['reuseWindows'] == true;
|
|
final shouldNotify =
|
|
params.containsKey('notify') && params['notify'] == true;
|
|
final page = params['page'];
|
|
if (canReuse &&
|
|
await _tryReuseExistingDevToolsInstance(
|
|
vmServiceUri,
|
|
page,
|
|
shouldNotify,
|
|
)) {
|
|
_emitLaunchEvent(
|
|
reused: true,
|
|
notified: shouldNotify,
|
|
pid: null,
|
|
machineMode: machineMode,
|
|
);
|
|
return {
|
|
'reused': true,
|
|
'notified': shouldNotify,
|
|
};
|
|
}
|
|
|
|
final uriParams = <String, dynamic>{};
|
|
|
|
// Copy over queryParams passed by the client
|
|
params['queryParams']?.forEach((key, value) => uriParams[key] = value);
|
|
|
|
// Add the URI to the VM service
|
|
uriParams['uri'] = vmServiceUri.toString();
|
|
|
|
final devToolsUri = Uri.parse(devToolsUrl);
|
|
final uriToLaunch = buildUriToLaunch(
|
|
devToolsUri,
|
|
page,
|
|
uriParams,
|
|
);
|
|
|
|
// TODO(dantup): When ChromeOS has support for tunneling all ports we can
|
|
// change this to always use the native browser for ChromeOS and may wish to
|
|
// handle this inside `browser_launcher`; https://crbug.com/848063.
|
|
final useNativeBrowser = _isChromeOS &&
|
|
_isAccessibleToChromeOSNativeBrowser(devToolsUri) &&
|
|
_isAccessibleToChromeOSNativeBrowser(vmServiceUri);
|
|
int? browserPid;
|
|
if (useNativeBrowser) {
|
|
await Process.start('x-www-browser', [uriToLaunch.toString()]);
|
|
} else {
|
|
final args = headlessMode
|
|
? [
|
|
'--headless',
|
|
// When running headless, Chrome will quit immediately after
|
|
// loading the page unless we have the debug port open.
|
|
'--remote-debugging-port=9223',
|
|
'--disable-gpu',
|
|
'--no-sandbox',
|
|
// When running on MacOS, Chrome may open system dialogs
|
|
// requesting credentials. This uses a mock keychain to avoid that
|
|
// dialog from blocking.
|
|
'--use-mock-keychain',
|
|
]
|
|
: <String>[];
|
|
final proc = await Chrome.start([uriToLaunch.toString()], args: args);
|
|
browserPid = proc.pid;
|
|
}
|
|
_emitLaunchEvent(
|
|
reused: false,
|
|
notified: false,
|
|
pid: browserPid!,
|
|
machineMode: machineMode);
|
|
return {
|
|
'reused': false,
|
|
'notified': false,
|
|
'pid': browserPid,
|
|
};
|
|
}
|
|
|
|
Future<void> _hookupMemoryProfiling(
|
|
Uri observatoryUri,
|
|
String profileFile, [
|
|
bool verboseMode = false,
|
|
]) async {
|
|
final service = await DevToolsUtils.connectToVmService(observatoryUri);
|
|
if (service == null) {
|
|
return;
|
|
}
|
|
|
|
final memoryProfiler = MemoryProfile(service, profileFile, verboseMode);
|
|
memoryProfiler.startPolling();
|
|
|
|
print('Writing memory profile samples to $profileFile...');
|
|
}
|
|
|
|
/// Tries to reuse an existing DevTools instance.
|
|
///
|
|
/// Because SSE connections have timeouts that prevent us knowing if a client
|
|
/// has really gone away for up to 30s, this method will attempt to ping
|
|
/// candidate first to see if it is still responsive.
|
|
Future<bool> _tryReuseExistingDevToolsInstance(
|
|
Uri vmServiceUri,
|
|
String? page,
|
|
bool notifyUser,
|
|
) async {
|
|
// First try to find a client that's already connected to this VM service,
|
|
// and just send the user a notification for that one.
|
|
final existingClient =
|
|
await clientManager.findExistingConnectedReusableClient(vmServiceUri);
|
|
if (existingClient != null) {
|
|
try {
|
|
if (page != null) {
|
|
existingClient.showPage(page);
|
|
}
|
|
if (notifyUser) {
|
|
existingClient.notify();
|
|
}
|
|
return true;
|
|
} catch (e) {
|
|
print('Failed to reuse existing connected DevTools client');
|
|
print(e);
|
|
}
|
|
}
|
|
|
|
final reusableClient = await clientManager.findReusableClient();
|
|
if (reusableClient != null) {
|
|
try {
|
|
reusableClient.connectToVmService(vmServiceUri, notifyUser);
|
|
return true;
|
|
} catch (e) {
|
|
print('Failed to reuse existing DevTools client');
|
|
print(e);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static String buildUriToLaunch(
|
|
Uri devToolsUri,
|
|
String? page,
|
|
Map<String, dynamic>? params,
|
|
) {
|
|
page ??= '';
|
|
var pathSep = devToolsUri.path.endsWith('/') ? '' : '/';
|
|
var newPath = '${devToolsUri.path}$pathSep$page';
|
|
var newParams = {
|
|
...devToolsUri.queryParameters,
|
|
...?params,
|
|
};
|
|
return devToolsUri
|
|
.replace(
|
|
path: newPath,
|
|
queryParameters: newParams.isNotEmpty ? newParams : null)
|
|
.toString();
|
|
}
|
|
|
|
/// Prints a launch event to stdout so consumers of the DevTools server
|
|
/// can see when clients are being launched/reused.
|
|
void _emitLaunchEvent(
|
|
{required bool reused,
|
|
required bool notified,
|
|
required int? pid,
|
|
required bool machineMode}) {
|
|
DevToolsUtils.printOutput(
|
|
null,
|
|
{
|
|
'event': 'client.launch',
|
|
'params': {
|
|
'reused': reused,
|
|
'notified': notified,
|
|
'pid': pid,
|
|
},
|
|
},
|
|
machineMode: machineMode,
|
|
);
|
|
}
|
|
|
|
bool _isAccessibleToChromeOSNativeBrowser(Uri uri) {
|
|
const tunneledPorts = {
|
|
8000,
|
|
8008,
|
|
8080,
|
|
8085,
|
|
8888,
|
|
9005,
|
|
3000,
|
|
4200,
|
|
5000
|
|
};
|
|
return uri.hasPort && tunneledPorts.contains(uri.port);
|
|
}
|
|
}
|