devtools_launcher.dart 7 KB
Newer Older
1 2 3 4
// 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.

5 6
// @dart = 2.8

7 8 9
import 'dart:async';

import 'package:meta/meta.dart';
10
import 'package:process/process.dart';
11 12 13

import 'base/io.dart' as io;
import 'base/logger.dart';
14
import 'base/platform.dart';
15
import 'convert.dart';
16
import 'persistent_tool_state.dart';
17 18 19 20
import 'resident_runner.dart';

/// An implementation of the devtools launcher that uses the server package.
///
21 22
/// This is implemented in `isolated/` to prevent the flutter_tool from needing
/// a devtools dependency in google3.
23 24
class DevtoolsServerLauncher extends DevtoolsLauncher {
  DevtoolsServerLauncher({
25
    @required Platform platform,
26 27 28
    @required ProcessManager processManager,
    @required String pubExecutable,
    @required Logger logger,
29
    @required PersistentToolState persistentToolState,
30
    @visibleForTesting io.HttpClient httpClient,
31 32
  })  : _processManager = processManager,
        _pubExecutable = pubExecutable,
33 34
        _logger = logger,
        _platform = platform,
35 36
        _persistentToolState = persistentToolState,
        _httpClient = httpClient ?? io.HttpClient();
37 38 39 40

  final ProcessManager _processManager;
  final String _pubExecutable;
  final Logger _logger;
41 42
  final Platform _platform;
  final PersistentToolState _persistentToolState;
43
  final io.HttpClient _httpClient;
44 45 46 47 48

  io.Process _devToolsProcess;

  static final RegExp _serveDevToolsPattern =
      RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
49
  static const String _pubHostedUrlKey = 'PUB_HOSTED_URL';
50 51

  @override
52 53
  Future<void> launch(Uri vmServiceUri) async {
    // Place this entire method in a try/catch that swallows exceptions because
54
    // this method is guaranteed not to return a Future that throws.
55 56
    try {
      bool offline = false;
57
      bool useOverrideUrl = false;
58
      try {
59 60 61 62
        Uri uri;
        if (_platform.environment.containsKey(_pubHostedUrlKey)) {
          useOverrideUrl = true;
          uri = Uri.parse(_platform.environment[_pubHostedUrlKey]);
63
        } else {
64 65 66 67 68 69 70
          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) {
          offline = true;
71 72 73
        }
      } on Exception {
        offline = true;
74 75 76 77 78 79 80 81 82 83
      } 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]}".'
        );
84 85
      }

86 87 88 89 90
      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.
91
        return;
92 93 94 95 96 97 98 99
      } 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;
        }
100 101 102 103 104 105 106
      }

      _devToolsProcess = await _processManager.start(<String>[
        _pubExecutable,
        'global',
        'run',
        'devtools',
107
        '--no-launch-browser',
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
        if (vmServiceUri != null) '--vm-uri=$vmServiceUri',
      ]);
      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);
132
      devToolsUrl = await completer.future;
133 134 135 136 137
    } on Exception catch (e, st) {
      _logger.printError('Failed to launch DevTools: $e', stackTrace: st);
    }
  }

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
  Future<bool> _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(<String>[
      _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<bool> _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;
164
    }
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
    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;
      }
      _persistentToolState.lastDevToolsActivationTime = DateTime.now();
      return true;
    } on Exception catch (e, _) {
      _logger.printError('Error running `pub global activate devtools`: $e');
      return false;
186 187
    } finally {
      status.stop();
188 189 190 191 192
    }
  }

  @override
  Future<DevToolsServerAddress> serve() async {
193 194 195
    if (activeDevToolsServer == null) {
      await launch(null);
    }
196
    return activeDevToolsServer;
197 198 199 200
  }

  @override
  Future<void> close() async {
201 202 203
    if (devToolsUrl != null) {
      devToolsUrl = null;
    }
204 205 206 207 208 209
    if (_devToolsProcess != null) {
      _devToolsProcess.kill();
      await _devToolsProcess.exitCode;
    }
  }
}