blob: 529ab866b93da3228736fe3eb57a9e81de7d1144 [file] [log] [blame]
// 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 `pub global activate` to
/// start a server instance.
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;
final Completer<void> _processStartCompleter = Completer<void>();
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<void> get processStart => _processStartCompleter.future;
@override
Future<void> launch(Uri vmServiceUri, {List<String> additionalArguments}) 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<void>();
if (response.statusCode != io.HttpStatus.ok) {
_logger.printTrace(
'Skipping devtools launch because pub.dev responded with HTTP '
'status code ${response.statusCode} instead of ${io.HttpStatus.ok}.',
);
offline = true;
}
} on Exception catch (e) {
_logger.printTrace(
'Skipping devtools launch because connecting to pub.dev failed with $e',
);
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]}".'
);
}
bool devToolsActive = await _checkForActiveDevTools();
if (!offline) {
await _activateDevTools(throttleUpdates: devToolsActive);
}
if (!devToolsActive && !offline) {
devToolsActive = await _checkForActiveDevTools();
}
if (!devToolsActive) {
// We don't have devtools installed and installing it failed;
// _activateDevTools will have reported the error already.
return;
}
_devToolsProcess = await _processManager.start(<String>[
_pubExecutable,
'global',
'run',
'devtools',
'--no-launch-browser',
if (vmServiceUri != null) '--vm-uri=$vmServiceUri',
...?additionalArguments,
]);
_processStartCompleter.complete();
final Completer<Uri> completer = Completer<Uri>();
_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);
}
}
static final RegExp _devToolsInstalledPattern = RegExp(r'^devtools ', multiLine: true);
/// Check if the DevTools package is already active by running "pub global list".
Future<bool> _checkForActiveDevTools() async {
final io.ProcessResult _pubGlobalListProcess = await _processManager.run(
<String>[ _pubExecutable, 'global', 'list' ],
);
return _pubGlobalListProcess.stdout.toString().contains(_devToolsInstalledPattern);
}
/// Helper method to activate the DevTools pub package.
///
/// If throttleUpdates is true, then this is a no-op if it was run in
/// the last twelve hours. It should be set to true if devtools is known
/// to already be installed.
///
/// Return value indicates if DevTools was installed or updated.
Future<bool> _activateDevTools({@required bool throttleUpdates}) async {
assert(throttleUpdates != null);
const Duration _throttleDuration = Duration(hours: 12);
if (throttleUpdates) {
if (_persistentToolState.lastDevToolsActivationTime != null &&
DateTime.now().difference(_persistentToolState.lastDevToolsActivationTime) < _throttleDuration) {
_logger.printTrace('DevTools activation throttled until ${_persistentToolState.lastDevToolsActivationTime.add(_throttleDuration).toLocal()}.');
return false; // Throttled.
}
}
final Status status = _logger.startProgress('Activating Dart DevTools...');
try {
final io.ProcessResult _devToolsActivateProcess = await _processManager
.run(<String>[
_pubExecutable,
'global',
'activate',
'devtools',
]);
if (_devToolsActivateProcess.exitCode != 0) {
_logger.printError(
'Error running `pub global activate devtools`:\n'
'${_devToolsActivateProcess.stderr}'
);
return false; // Failed to activate.
}
_persistentToolState.lastDevToolsActivation = DateTime.now();
return true; // Activation succeeded!
} on Exception catch (e, _) {
_logger.printError('Error running `pub global activate devtools`: $e');
return false;
} finally {
status.stop();
}
}
@override
Future<DevToolsServerAddress> serve() async {
if (activeDevToolsServer == null) {
await launch(null);
}
return activeDevToolsServer;
}
@override
Future<void> close() async {
if (devToolsUrl != null) {
devToolsUrl = null;
}
if (_devToolsProcess != null) {
_devToolsProcess.kill();
await _devToolsProcess.exitCode;
}
}
}