// 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:meta/meta.dart'; import 'package:process/process.dart'; import 'base/bot_detector.dart'; import 'base/common.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 `pub global activate` to /// start a server instance. class DevtoolsServerLauncher extends DevtoolsLauncher { DevtoolsServerLauncher({ required ProcessManager processManager, required String dartExecutable, required Logger? logger, required BotDetector botDetector, }) : _processManager = processManager, _dartExecutable = dartExecutable, _logger = logger, _botDetector = botDetector; final ProcessManager _processManager; final String _dartExecutable; final Logger? _logger; final BotDetector _botDetector; final Completer<void> _processStartCompleter = Completer<void>(); io.Process? _devToolsProcess; bool _devToolsProcessKilled = false; @visibleForTesting Future<void>? devToolsProcessExit; static final RegExp _serveDevToolsPattern = RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+?)\.?$'); @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 { _devToolsProcess = await _processManager.start(<String>[ _dartExecutable, '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) { final String url = match[1]!; completer.complete(Uri.parse(url)); } }); _devToolsProcess!.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen(_logger!.printError); final bool runningOnBot = await _botDetector.isRunningOnBot; devToolsProcessExit = _devToolsProcess!.exitCode.then( (int exitCode) { if (!_devToolsProcessKilled && runningOnBot) { throwToolExit('DevTools process failed: exitCode=$exitCode'); } } ); devToolsUrl = await completer.future; } on Exception catch (e, st) { _logger!.printError('Failed to launch DevTools: $e', stackTrace: st); } } @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) { _devToolsProcessKilled = true; _devToolsProcess!.kill(); } } }