resident_devtools_handler.dart 9.86 KB
Newer Older
1 2 3 4 5 6 7 8
// 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';

9
import 'package:browser_launcher/browser_launcher.dart';
10 11
import 'package:meta/meta.dart';

12
import 'base/common.dart';
13
import 'base/logger.dart';
14
import 'build_info.dart';
15 16 17
import 'resident_runner.dart';
import 'vmservice.dart';

18 19 20 21 22 23
typedef ResidentDevtoolsHandlerFactory = ResidentDevtoolsHandler Function(DevtoolsLauncher, ResidentRunner, Logger);

ResidentDevtoolsHandler createDefaultHandler(DevtoolsLauncher launcher, ResidentRunner runner, Logger logger) {
  return FlutterResidentDevtoolsHandler(launcher, runner, logger);
}

24 25
/// Helper class to manage the life-cycle of devtools and its interaction with
/// the resident runner.
26 27 28 29
abstract class ResidentDevtoolsHandler {
  /// The current devtools server, or null if one is not running.
  DevToolsServerAddress get activeDevToolsServer;

30 31 32 33 34 35
  /// Whether it's ok to announce the [activeDevToolsServer].
  ///
  /// This should only return true once all the devices have been notified
  /// of the DevTools.
  bool get readyToAnnounce;

36 37
  Future<void> hotRestart(List<FlutterDevice> flutterDevices);

38 39 40 41 42 43
  Future<void> serveAndAnnounceDevTools({
    Uri devToolsServerAddress,
    @required List<FlutterDevice> flutterDevices,
  });

  bool launchDevToolsInBrowser({@required List<FlutterDevice> flutterDevices});
44 45 46 47 48 49

  Future<void> shutdown();
}

class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
  FlutterResidentDevtoolsHandler(this._devToolsLauncher, this._residentRunner, this._logger);
50

51 52
  static const Duration launchInBrowserTimeout = Duration(seconds: 15);

53 54 55 56 57 58
  final DevtoolsLauncher _devToolsLauncher;
  final ResidentRunner _residentRunner;
  final Logger _logger;
  bool _shutdown = false;
  bool _served = false;

59 60 61
  @visibleForTesting
  bool launchedInBrowser = false;

62
  @override
63 64 65 66
  DevToolsServerAddress get activeDevToolsServer {
    assert(!_readyToAnnounce || _devToolsLauncher?.activeDevToolsServer != null);
    return _devToolsLauncher?.activeDevToolsServer;
  }
67

68 69 70 71
  @override
  bool get readyToAnnounce => _readyToAnnounce;
  bool _readyToAnnounce = false;

72
  // This must be guaranteed not to return a Future that fails.
73
  @override
74 75 76 77
  Future<void> serveAndAnnounceDevTools({
    Uri devToolsServerAddress,
    @required List<FlutterDevice> flutterDevices,
  }) async {
78
    assert(!_readyToAnnounce);
79 80 81 82 83 84 85
    if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) {
      return;
    }
    if (devToolsServerAddress != null) {
      _devToolsLauncher.devToolsUrl = devToolsServerAddress;
    } else {
      await _devToolsLauncher.serve();
86
      _served = true;
87 88
    }
    await _devToolsLauncher.ready;
89 90 91
    // Do not attempt to print debugger list if the connection has failed or if we're shutting down.
    if (_devToolsLauncher.activeDevToolsServer == null || _shutdown) {
      assert(!_readyToAnnounce);
92 93
      return;
    }
94 95 96
    final List<FlutterDevice> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
    await _maybeCallDevToolsUriServiceExtension(devicesWithExtension);
    await _callConnectedVmServiceUriExtension(devicesWithExtension);
97 98 99 100
    if (_shutdown) {
      // If we're shutting down, no point reporting the debugger list.
      return;
    }
101
    _readyToAnnounce = true;
102
    assert(_devToolsLauncher.activeDevToolsServer != null);
103 104 105 106 107 108 109
    if (_residentRunner.reportedDebuggers) {
      // Since the DevTools only just became available, we haven't had a chance to
      // report their URLs yet. Do so now.
      _residentRunner.printDebuggerList(includeObservatory: false);
    }
  }

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
  // This must be guaranteed not to return a Future that fails.
  @override
  bool launchDevToolsInBrowser({@required List<FlutterDevice> flutterDevices}) {
    if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) {
      return false;
    }
    if (_devToolsLauncher.devToolsUrl == null) {
      _logger.startProgress('Waiting for Flutter DevTools to be served...');
      unawaited(_devToolsLauncher.ready.then((_) {
        _launchDevToolsForDevices(flutterDevices);
      }));
    } else {
      _launchDevToolsForDevices(flutterDevices);
    }
    return true;
  }

  void _launchDevToolsForDevices(List<FlutterDevice> flutterDevices) {
    assert(activeDevToolsServer != null);
    for (final FlutterDevice device in flutterDevices) {
      final String devToolsUrl = activeDevToolsServer.uri?.replace(
        queryParameters: <String, dynamic>{'uri': '${device.vmService.httpAddress}'},
      ).toString();
      _logger.printStatus('Launching Flutter DevTools for ${device.device.name} at $devToolsUrl');
      unawaited(Chrome.start(<String>[devToolsUrl]));
    }
    launchedInBrowser = true;
  }

139 140 141 142 143 144
  Future<void> _maybeCallDevToolsUriServiceExtension(
    List<FlutterDevice> flutterDevices,
  ) async {
    if (_devToolsLauncher?.activeDevToolsServer == null) {
      return;
    }
145 146
    await Future.wait(<Future<void>>[
      for (final FlutterDevice device in flutterDevices)
147
        if (device.vmService != null) _callDevToolsUriExtension(device),
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
    ]);
  }

  Future<void> _callDevToolsUriExtension(
    FlutterDevice device,
  ) async {
    try {
      await _invokeRpcOnFirstView(
        'ext.flutter.activeDevToolsServerAddress',
        device: device,
        params: <String, dynamic>{
          'value': _devToolsLauncher.activeDevToolsServer.uri.toString(),
        },
      );
    } on Exception catch (e) {
      _logger.printError(
        'Failed to set DevTools server address: ${e.toString()}. Deep links to'
        ' DevTools will not show in Flutter errors.',
      );
    }
  }

170 171
  Future<List<FlutterDevice>> _devicesWithExtensions(List<FlutterDevice> flutterDevices) async {
    final List<FlutterDevice> devices = await Future.wait(<Future<FlutterDevice>>[
172
      for (final FlutterDevice device in flutterDevices) _waitForExtensionsForDevice(device)
173
    ]);
174 175 176 177 178 179 180
    return devices.where((FlutterDevice device) => device != null).toList();
  }

  /// Returns null if the service extension cannot be found on the device.
  Future<FlutterDevice> _waitForExtensionsForDevice(FlutterDevice flutterDevice) async {
    const String extension = 'ext.flutter.connectedVmServiceUri';
    try {
181 182 183
      await flutterDevice.vmService?.findExtensionIsolate(
        extension,
      );
184 185 186 187 188 189 190 191 192
      return flutterDevice;
    } on VmServiceDisappearedException {
      _logger.printTrace(
        'The VM Service for ${flutterDevice.device} disappeared while trying to'
        ' find the $extension service extension. Skipping subsequent DevTools '
        'setup for this device.',
      );
      return null;
    }
193 194 195
  }

  Future<void> _callConnectedVmServiceUriExtension(List<FlutterDevice> flutterDevices) async {
196 197
    await Future.wait(<Future<void>>[
      for (final FlutterDevice device in flutterDevices)
198
        if (device.vmService != null) _callConnectedVmServiceExtension(device),
199 200 201 202
    ]);
  }

  Future<void> _callConnectedVmServiceExtension(FlutterDevice device) async {
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    final Uri uri = device.vmService.httpAddress ?? device.vmService.wsAddress;
    if (uri == null) {
      return;
    }
    try {
      await _invokeRpcOnFirstView(
        'ext.flutter.connectedVmServiceUri',
        device: device,
        params: <String, dynamic>{
          'value': uri.toString(),
        },
      );
    } on Exception catch (e) {
      _logger.printError(e.toString());
      _logger.printError(
        'Failed to set vm service URI: ${e.toString()}. Deep links to DevTools'
        ' will not show in Flutter errors.',
      );
221 222 223
    }
  }

224 225
  Future<void> _invokeRpcOnFirstView(
    String method, {
226 227 228
    @required FlutterDevice device,
    @required Map<String, dynamic> params,
  }) async {
229 230 231 232 233 234
    if (device.targetPlatform == TargetPlatform.web_javascript) {
      return device.vmService.callMethodWrapper(
        method,
        args: params,
      );
    }
235
    final List<FlutterView> views = await device.vmService.getFlutterViews();
236 237 238
    if (views.isEmpty) {
      return;
    }
239 240 241 242 243
    await device.vmService.invokeFlutterExtensionRpcRaw(
      method,
      args: params,
      isolateId: views.first.uiIsolate.id,
    );
244 245
  }

246
  @override
247
  Future<void> hotRestart(List<FlutterDevice> flutterDevices) async {
248
    final List<FlutterDevice> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
249
    await Future.wait(<Future<void>>[
250 251
      _maybeCallDevToolsUriServiceExtension(devicesWithExtension),
      _callConnectedVmServiceUriExtension(devicesWithExtension),
252 253 254
    ]);
  }

255
  @override
256 257 258 259 260
  Future<void> shutdown() async {
    if (_devToolsLauncher == null || _shutdown || !_served) {
      return;
    }
    _shutdown = true;
261
    _readyToAnnounce = false;
262 263 264 265
    await _devToolsLauncher.close();
  }
}

266 267 268 269 270 271 272
@visibleForTesting
NoOpDevtoolsHandler createNoOpHandler(DevtoolsLauncher launcher, ResidentRunner runner, Logger logger) {
  return NoOpDevtoolsHandler();
}

@visibleForTesting
class NoOpDevtoolsHandler implements ResidentDevtoolsHandler {
273 274
  bool wasShutdown = false;

275 276 277
  @override
  DevToolsServerAddress get activeDevToolsServer => null;

278 279 280
  @override
  bool get readyToAnnounce => false;

281 282 283 284 285 286 287 288 289 290
  @override
  Future<void> hotRestart(List<FlutterDevice> flutterDevices) async {
    return;
  }

  @override
  Future<void> serveAndAnnounceDevTools({Uri devToolsServerAddress, List<FlutterDevice> flutterDevices}) async {
    return;
  }

291 292 293 294 295
  @override
  bool launchDevToolsInBrowser({List<FlutterDevice> flutterDevices}) {
    return false;
  }

296 297
  @override
  Future<void> shutdown() async {
298
    wasShutdown = true;
299 300 301
    return;
  }
}
302 303 304 305 306 307 308 309 310 311

/// Convert a [URI] with query parameters into a display format instead
/// of the default URI encoding.
String urlToDisplayString(Uri uri) {
  final StringBuffer base = StringBuffer(uri.replace(
    queryParameters: <String, String>{},
  ).toString());
  base.write(uri.queryParameters.keys.map((String key) => '$key=${uri.queryParameters[key]}').join('&'));
  return base.toString();
}