| // 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 argDisableServiceOriginCheck = 'disable-service-origin-check'; | |
| 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.', | |
| ) | |
| ..addFlag( | |
| argDisableServiceOriginCheck, | |
| negatable: false, | |
| help: 'Disables the requirements for Host and Origin header validation ' | |
| 'for DevTools server connections. Host and Origin validation helps ' | |
| 'protect against DNS-rebinding and CSRF attacks, so it is not ' | |
| 'recommended to disable them.', | |
| ) | |
| ..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, | |
| bool disableServiceOriginCheck = false, | |
| }) 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, | |
| ); | |
| 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!; | |
| final serverUri = Uri( | |
| scheme: 'http', | |
| host: hostname, | |
| port: server.port, | |
| ); | |
| handler ??= await defaultHandler( | |
| buildDir: customDevToolsPath, | |
| clientManager: clientManager, | |
| dtd: dtdInfo, | |
| devtoolsExtensionsManager: ExtensionsManager(), | |
| disableServiceOriginCheck: disableServiceOriginCheck, | |
| serverUri: serverUri, | |
| ); | |
| 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 bool disableServiceOriginCheck = | |
| args.wasParsed(argDisableServiceOriginCheck) | |
| ? args[argDisableServiceOriginCheck] | |
| : 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, | |
| disableServiceOriginCheck: disableServiceOriginCheck, | |
| ); | |
| } | |
| 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); | |
| } | |
| } |