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

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

import 'base/logger.dart';
11
import 'build_info.dart';
12 13 14
import 'resident_runner.dart';
import 'vmservice.dart';

15
typedef ResidentDevtoolsHandlerFactory = ResidentDevtoolsHandler Function(DevtoolsLauncher?, ResidentRunner, Logger);
16

17
ResidentDevtoolsHandler createDefaultHandler(DevtoolsLauncher? launcher, ResidentRunner runner, Logger logger) {
18 19 20
  return FlutterResidentDevtoolsHandler(launcher, runner, logger);
}

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

27 28 29 30 31 32
  /// 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;

33
  Future<void> hotRestart(List<FlutterDevice?> flutterDevices);
34

35
  Future<void> serveAndAnnounceDevTools({
36 37
    Uri? devToolsServerAddress,
    required List<FlutterDevice?> flutterDevices,
38 39
  });

40
  bool launchDevToolsInBrowser({required List<FlutterDevice?> flutterDevices});
41 42 43 44 45 46

  Future<void> shutdown();
}

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

48 49
  static const Duration launchInBrowserTimeout = Duration(seconds: 15);

50
  final DevtoolsLauncher? _devToolsLauncher;
51 52 53 54 55
  final ResidentRunner _residentRunner;
  final Logger _logger;
  bool _shutdown = false;
  bool _served = false;

56 57 58
  @visibleForTesting
  bool launchedInBrowser = false;

59
  @override
60
  DevToolsServerAddress? get activeDevToolsServer {
61 62 63
    assert(!_readyToAnnounce || _devToolsLauncher?.activeDevToolsServer != null);
    return _devToolsLauncher?.activeDevToolsServer;
  }
64

65 66 67 68
  @override
  bool get readyToAnnounce => _readyToAnnounce;
  bool _readyToAnnounce = false;

69
  // This must be guaranteed not to return a Future that fails.
70
  @override
71
  Future<void> serveAndAnnounceDevTools({
72 73
    Uri? devToolsServerAddress,
    required List<FlutterDevice?> flutterDevices,
74
  }) async {
75
    assert(!_readyToAnnounce);
76 77 78 79
    if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) {
      return;
    }
    if (devToolsServerAddress != null) {
80
      _devToolsLauncher!.devToolsUrl = devToolsServerAddress;
81
    } else {
82
      await _devToolsLauncher!.serve();
83
      _served = true;
84
    }
85
    await _devToolsLauncher!.ready;
86
    // Do not attempt to print debugger list if the connection has failed or if we're shutting down.
87
    if (_devToolsLauncher!.activeDevToolsServer == null || _shutdown) {
88
      assert(!_readyToAnnounce);
89 90
      return;
    }
91
    final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
92 93
    await _maybeCallDevToolsUriServiceExtension(devicesWithExtension);
    await _callConnectedVmServiceUriExtension(devicesWithExtension);
94

95 96 97 98
    if (_shutdown) {
      // If we're shutting down, no point reporting the debugger list.
      return;
    }
99
    _readyToAnnounce = true;
100
    assert(_devToolsLauncher!.activeDevToolsServer != null);
101 102 103 104 105 106 107 108 109 110 111 112 113

    final Uri? devToolsUrl = _devToolsLauncher!.devToolsUrl;
    if (devToolsUrl != null) {
      for (final FlutterDevice? device in devicesWithExtension) {
        if (device == null) {
          continue;
        }
        // Notify the DDS instances that there's a DevTools instance available so they can correctly
        // redirect DevTools related requests.
        device.device?.dds.setExternalDevToolsUri(devToolsUrl);
      }
    }

114 115 116
    if (_residentRunner.reportedDebuggers) {
      // Since the DevTools only just became available, we haven't had a chance to
      // report their URLs yet. Do so now.
117
      _residentRunner.printDebuggerList(includeVmService: false);
118 119 120
    }
  }

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

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

150
  Future<void> _maybeCallDevToolsUriServiceExtension(
151
    List<FlutterDevice?> flutterDevices,
152 153 154 155
  ) async {
    if (_devToolsLauncher?.activeDevToolsServer == null) {
      return;
    }
156
    await Future.wait(<Future<void>>[
157 158
      for (final FlutterDevice? device in flutterDevices)
        if (device?.vmService != null) _callDevToolsUriExtension(device!),
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>{
170
          'value': _devToolsLauncher!.activeDevToolsServer!.uri.toString(),
171 172 173 174
        },
      );
    } on Exception catch (e) {
      _logger.printError(
175
        'Failed to set DevTools server address: $e. Deep links to'
176 177 178 179 180
        ' DevTools will not show in Flutter errors.',
      );
    }
  }

181 182 183
  Future<List<FlutterDevice?>> _devicesWithExtensions(List<FlutterDevice?> flutterDevices) async {
    return Future.wait(<Future<FlutterDevice?>>[
      for (final FlutterDevice? device in flutterDevices) _waitForExtensionsForDevice(device!),
184
    ]);
185 186 187
  }

  /// Returns null if the service extension cannot be found on the device.
188
  Future<FlutterDevice?> _waitForExtensionsForDevice(FlutterDevice flutterDevice) async {
189 190
    const String extension = 'ext.flutter.connectedVmServiceUri';
    try {
191 192 193
      await flutterDevice.vmService?.findExtensionIsolate(
        extension,
      );
194 195 196 197 198 199 200 201 202
      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;
    }
203 204
  }

205
  Future<void> _callConnectedVmServiceUriExtension(List<FlutterDevice?> flutterDevices) async {
206
    await Future.wait(<Future<void>>[
207 208
      for (final FlutterDevice? device in flutterDevices)
        if (device?.vmService != null) _callConnectedVmServiceExtension(device!),
209 210 211 212
    ]);
  }

  Future<void> _callConnectedVmServiceExtension(FlutterDevice device) async {
213
    final Uri? uri = device.vmService!.httpAddress ?? device.vmService!.wsAddress;
214 215 216 217 218 219 220 221 222 223 224 225 226 227
    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(
228
        'Failed to set vm service URI: $e. Deep links to DevTools'
229 230
        ' will not show in Flutter errors.',
      );
231 232 233
    }
  }

234 235
  Future<void> _invokeRpcOnFirstView(
    String method, {
236 237
    required FlutterDevice device,
    required Map<String, dynamic> params,
238
  }) async {
239
    if (device.targetPlatform == TargetPlatform.web_javascript) {
240
      await device.vmService!.callMethodWrapper(
241 242 243
        method,
        args: params,
      );
244
      return;
245
    }
246
    final List<FlutterView> views = await device.vmService!.getFlutterViews();
247 248 249
    if (views.isEmpty) {
      return;
    }
250
    await device.vmService!.invokeFlutterExtensionRpcRaw(
251 252
      method,
      args: params,
253
      isolateId: views.first.uiIsolate!.id!,
254
    );
255 256
  }

257
  @override
258 259
  Future<void> hotRestart(List<FlutterDevice?> flutterDevices) async {
    final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
260
    await Future.wait(<Future<void>>[
261 262
      _maybeCallDevToolsUriServiceExtension(devicesWithExtension),
      _callConnectedVmServiceUriExtension(devicesWithExtension),
263 264 265
    ]);
  }

266
  @override
267 268 269 270 271
  Future<void> shutdown() async {
    if (_devToolsLauncher == null || _shutdown || !_served) {
      return;
    }
    _shutdown = true;
272
    _readyToAnnounce = false;
273
    await _devToolsLauncher!.close();
274 275 276
  }
}

277
@visibleForTesting
278
NoOpDevtoolsHandler createNoOpHandler(DevtoolsLauncher? launcher, ResidentRunner runner, Logger logger) {
279 280 281 282 283
  return NoOpDevtoolsHandler();
}

@visibleForTesting
class NoOpDevtoolsHandler implements ResidentDevtoolsHandler {
284 285
  bool wasShutdown = false;

286
  @override
287
  DevToolsServerAddress? get activeDevToolsServer => null;
288

289 290 291
  @override
  bool get readyToAnnounce => false;

292
  @override
293
  Future<void> hotRestart(List<FlutterDevice?> flutterDevices) async {
294 295 296 297
    return;
  }

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

302
  @override
303
  bool launchDevToolsInBrowser({List<FlutterDevice?>? flutterDevices}) {
304 305 306
    return false;
  }

307 308
  @override
  Future<void> shutdown() async {
309
    wasShutdown = true;
310 311 312
    return;
  }
}
313 314 315 316 317 318 319 320 321 322

/// 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();
}