// Copyright 2014 The Flutter 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:browser_launcher/browser_launcher.dart'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; import 'base/io.dart' as io; import 'base/logger.dart'; import 'convert.dart'; import 'resident_runner.dart'; /// An implementation of the devtools launcher that uses the server package. /// /// This is implemented in isolated to prevent the flutter_tool from needing /// a devtools dep in google3. class DevtoolsServerLauncher extends DevtoolsLauncher { DevtoolsServerLauncher({ @required ProcessManager processManager, @required String pubExecutable, @required Logger logger, }) : _processManager = processManager, _pubExecutable = pubExecutable, _logger = logger; final ProcessManager _processManager; final String _pubExecutable; final Logger _logger; io.Process _devToolsProcess; Uri _devToolsUri; static final RegExp _serveDevToolsPattern = RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)'); @override Future launch(Uri vmServiceUri, {bool openInBrowser = false}) async { if (_devToolsProcess != null && _devToolsUri != null) { // DevTools is already running. if (openInBrowser) { await Chrome.start([_devToolsUri.toString()]); } return; } final Status status = _logger.startProgress( 'Activating Dart DevTools...', ); try { // TODO(kenz): https://github.com/dart-lang/pub/issues/2791 - calling `pub // global activate` adds ~ 4.5 seconds of latency. final io.ProcessResult _devToolsActivateProcess = await _processManager.run([ _pubExecutable, 'global', 'activate', 'devtools' ]); if (_devToolsActivateProcess.exitCode != 0) { status.cancel(); _logger.printError('Error running `pub global activate ' 'devtools`:\n${_devToolsActivateProcess.stderr}'); return; } status.stop(); _devToolsProcess = await _processManager.start([ _pubExecutable, 'global', 'run', 'devtools', if (!openInBrowser) '--no-launch-browser', if (vmServiceUri != null) '--vm-uri=$vmServiceUri', ]); final Completer completer = Completer(); _devToolsProcess.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { final Match match = _serveDevToolsPattern.firstMatch(line); if (match != null) { // We are trying to pull "http://127.0.0.1:9101" from "Serving // DevTools at http://127.0.0.1:9101.". `match[1]` will return // "http://127.0.0.1:9101.", and we need to trim the trailing period // so that we don't throw an exception from `Uri.parse`. String uri = match[1]; if (uri.endsWith('.')) { uri = uri.substring(0, uri.length - 1); } completer.complete(Uri.parse(uri)); } _logger.printStatus(line); }); _devToolsProcess.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen(_logger.printError); _devToolsUri = await completer.future; } on Exception catch (e, st) { status.cancel(); _logger.printError('Failed to launch DevTools: $e', stackTrace: st); } } @override Future serve({bool openInBrowser = false}) async { await launch(null, openInBrowser: openInBrowser); if (_devToolsUri == null) { return null; } return DevToolsServerAddress(_devToolsUri.host, _devToolsUri.port); } @override Future close() async { if (_devToolsProcess != null) { _devToolsProcess.kill(); await _devToolsProcess.exitCode; } } }