// 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. // @dart = 2.8 import 'dart:async'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; import 'base/io.dart' as io; import 'base/logger.dart'; import 'base/platform.dart'; import 'convert.dart'; import 'persistent_tool_state.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 dependency in google3. class DevtoolsServerLauncher extends DevtoolsLauncher { DevtoolsServerLauncher({ @required Platform platform, @required ProcessManager processManager, @required String pubExecutable, @required Logger logger, @required PersistentToolState persistentToolState, @visibleForTesting io.HttpClient httpClient, }) : _processManager = processManager, _pubExecutable = pubExecutable, _logger = logger, _platform = platform, _persistentToolState = persistentToolState, _httpClient = httpClient ?? io.HttpClient(); final ProcessManager _processManager; final String _pubExecutable; final Logger _logger; final Platform _platform; final PersistentToolState _persistentToolState; final io.HttpClient _httpClient; io.Process _devToolsProcess; static final RegExp _serveDevToolsPattern = RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)'); static const String _pubHostedUrlKey = 'PUB_HOSTED_URL'; @override Future launch(Uri vmServiceUri) async { // Place this entire method in a try/catch that swallows exceptions because // this method is guaranteed not to return a Future that throws. try { bool offline = false; bool useOverrideUrl = false; try { Uri uri; if (_platform.environment.containsKey(_pubHostedUrlKey)) { useOverrideUrl = true; uri = Uri.parse(_platform.environment[_pubHostedUrlKey]); } else { uri = Uri.https('pub.dev', ''); } final io.HttpClientRequest request = await _httpClient.headUrl(uri); final io.HttpClientResponse response = await request.close(); await response.drain(); if (response.statusCode != io.HttpStatus.ok) { offline = true; } } on Exception { offline = true; } on ArgumentError { if (!useOverrideUrl) { rethrow; } // The user supplied a custom pub URL that was invalid, pretend to be offline // and inform them that the URL was invalid. offline = true; _logger.printError( 'PUB_HOSTED_URL was set to an invalid URL: "${_platform.environment[_pubHostedUrlKey]}".' ); } if (offline) { // TODO(kenz): we should launch an already activated version of DevTools // here, if available, once DevTools has offline support. DevTools does // not work without internet currently due to the failed request of a // couple scripts. See https://github.com/flutter/devtools/issues/2420. return; } else { final bool didActivateDevTools = await _activateDevTools(); final bool devToolsActive = await _checkForActiveDevTools(); if (!didActivateDevTools && !devToolsActive) { // At this point, we failed to activate the DevTools package and the // package is not already active. return; } } _devToolsProcess = await _processManager.start([ _pubExecutable, 'global', 'run', 'devtools', '--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)); } }); _devToolsProcess.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen(_logger.printError); devToolsUrl = await completer.future; } on Exception catch (e, st) { _logger.printError('Failed to launch DevTools: $e', stackTrace: st); } } Future _checkForActiveDevTools() async { // We are offline, and cannot activate DevTools, so check if the DevTools // package is already active. final io.ProcessResult _pubGlobalListProcess = await _processManager.run([ _pubExecutable, 'global', 'list', ]); if (_pubGlobalListProcess.stdout.toString().contains('devtools ')) { return true; } return false; } /// Helper method to activate the DevTools pub package. /// /// Returns a bool indicating whether or not the package was successfully /// activated from pub. Future _activateDevTools() async { final DateTime now = DateTime.now(); // Only attempt to activate DevTools twice a day. final bool shouldActivate = _persistentToolState.lastDevToolsActivationTime == null || now.difference(_persistentToolState.lastDevToolsActivationTime).inHours >= 12; if (!shouldActivate) { return false; } final Status status = _logger.startProgress( 'Activating Dart DevTools...', ); try { final io.ProcessResult _devToolsActivateProcess = await _processManager .run([ _pubExecutable, 'global', 'activate', 'devtools' ]); if (_devToolsActivateProcess.exitCode != 0) { _logger.printError('Error running `pub global activate ' 'devtools`:\n${_devToolsActivateProcess.stderr}'); return false; } _persistentToolState.lastDevToolsActivationTime = DateTime.now(); return true; } on Exception catch (e, _) { _logger.printError('Error running `pub global activate devtools`: $e'); return false; } finally { status.stop(); } } @override Future serve() async { if (activeDevToolsServer == null) { await launch(null); } return activeDevToolsServer; } @override Future close() async { if (devToolsUrl != null) { devToolsUrl = null; } if (_devToolsProcess != null) { _devToolsProcess.kill(); await _devToolsProcess.exitCode; } } }