fallback_discovery.dart 6.28 KB
Newer Older
1 2 3 4 5 6 7
// 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 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart';

8
import '../base/io.dart';
9 10 11 12 13
import '../base/logger.dart';
import '../device.dart';
import '../protocol_discovery.dart';
import '../reporting/reporting.dart';

14 15
typedef VmServiceConnector = Future<VmService> Function(String, {Log log});

16 17 18
/// A protocol for discovery of a vmservice on an attached iOS device with
/// multiple fallbacks.
///
19
/// First, it tries to discover a vmservice by assigning a
20 21 22 23 24
/// specific port and then attempt to connect. This may fail if the port is
/// not available. This port value should be either random, or otherwise
/// generated with application specific input. This reduces the chance of
/// accidentally connecting to another running flutter application.
///
25 26
/// If that does not work, attempt to scan logs from the attached debugger
/// and parse the connected port logged by the engine.
27 28 29 30 31
class FallbackDiscovery {
  FallbackDiscovery({
    @required DevicePortForwarder portForwarder,
    @required Logger logger,
    @required ProtocolDiscovery protocolDiscovery,
32
    @required Usage flutterUsage,
33
    @required VmServiceConnector vmServiceConnectUri,
34
    Duration pollingDelay,
35 36 37
  }) : _logger = logger,
       _portForwarder = portForwarder,
       _protocolDiscovery = protocolDiscovery,
38
       _flutterUsage = flutterUsage,
39
       _vmServiceConnectUri = vmServiceConnectUri,
40
       _pollingDelay = pollingDelay ?? const Duration(seconds: 2);
41 42 43 44 45 46

  static const String _kEventName = 'ios-handshake';

  final DevicePortForwarder _portForwarder;
  final Logger _logger;
  final ProtocolDiscovery _protocolDiscovery;
47
  final Usage _flutterUsage;
48 49
  final VmServiceConnector  _vmServiceConnectUri;
  final Duration _pollingDelay;
50 51 52 53 54

  /// Attempt to discover the observatory port.
  Future<Uri> discover({
    @required int assumedDevicePort,
    @required String packageId,
55
    @required Device device,
56 57 58 59 60 61 62 63 64 65 66 67 68 69
    @required bool usesIpv6,
    @required int hostVmservicePort,
    @required String packageName,
  }) async {
    final Uri result = await _attemptServiceConnection(
      assumedDevicePort: assumedDevicePort,
      hostVmservicePort: hostVmservicePort,
      packageName: packageName,
    );
    if (result != null) {
      return result;
    }

    try {
70
      final Uri result = await _protocolDiscovery.uri;
71
      if (result != null) {
72 73
        UsageEvent(
          _kEventName,
74
          'log-success',
75
          flutterUsage: _flutterUsage,
76
        ).send();
77 78
        return result;
      }
79 80 81
    } on ArgumentError {
      // In the event of an invalid InternetAddress, this code attempts to catch
      // an ArgumentError from protocol_discovery.dart
82 83 84
    } on Exception catch (err) {
      _logger.printTrace(err.toString());
    }
85
    _logger.printTrace('Failed to connect with log scanning');
86 87
    UsageEvent(
      _kEventName,
88
      'log-failure',
89
      flutterUsage: _flutterUsage,
90
    ).send();
91 92 93 94 95 96 97 98

    return null;
  }

  // Attempt to connect to the VM service and find an isolate with a matching `packageName`.
  // Returns `null` if no connection can be made.
  Future<Uri> _attemptServiceConnection({
    @required int assumedDevicePort,
99
    @required int hostVmservicePort,
100 101 102 103 104
    @required String packageName,
  }) async {
    int hostPort;
    Uri assumedWsUri;
    try {
105 106 107 108
      hostPort = await _portForwarder.forward(
        assumedDevicePort,
        hostPort: hostVmservicePort,
      );
109 110 111
      assumedWsUri = Uri.parse('ws://localhost:$hostPort/ws');
    } on Exception catch (err) {
      _logger.printTrace(err.toString());
112
      _logger.printTrace('Failed to connect directly, falling back to log scanning');
113
      _sendFailureEvent(err, assumedDevicePort);
114 115 116 117 118
      return null;
    }

    // Attempt to connect to the VM service 5 times.
    int attempts = 0;
119
    Exception firstException;
120
    VmService vmService;
121 122
    while (attempts < 5) {
      try {
123
        vmService = await _vmServiceConnectUri(
124 125
          assumedWsUri.toString(),
        );
126 127
        final VM vm = await vmService.getVM();
        for (final IsolateRef isolateRefs in vm.isolates) {
128
          final Isolate isolateResponse = await vmService.getIsolate(
129 130
            isolateRefs.id,
          );
131
          final LibraryRef library = isolateResponse.rootLib;
132 133 134
          if (library != null &&
             (library.uri.startsWith('package:$packageName') ||
              library.uri.startsWith(RegExp(r'file:\/\/\/.*\/' + packageName)))) {
135 136 137
            UsageEvent(
              _kEventName,
              'success',
138
              flutterUsage: _flutterUsage,
139
            ).send();
140

141 142
            // This vmService instance must be disposed of, otherwise DDS will
            // fail to start.
143
            vmService.dispose();
144 145 146 147 148
            return Uri.parse('http://localhost:$hostPort');
          }
        }
      } on Exception catch (err) {
        // No action, we might have failed to connect.
149
        firstException ??= err;
150
        _logger.printTrace(err.toString());
151 152 153 154
      } finally {
        // This vmService instance must be disposed of, otherwise DDS will
        // fail to start.
        vmService?.dispose();
155 156 157 158 159 160
      }

      // No exponential backoff is used here to keep the amount of time the
      // tool waits for a connection to be reasonable. If the vmservice cannot
      // be connected to in this way, the mDNS discovery must be reached
      // sooner rather than later.
161
      await Future<void>.delayed(_pollingDelay);
162 163
      attempts += 1;
    }
164
    _logger.printTrace('Failed to connect directly, falling back to log scanning');
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
    _sendFailureEvent(firstException, assumedDevicePort);
    return null;
  }

  void _sendFailureEvent(Exception err, int assumedDevicePort) {
    String eventAction;
    String eventLabel;
    if (err == null) {
      eventAction = 'failure-attempts-exhausted';
      eventLabel = assumedDevicePort.toString();
    } else if (err is HttpException) {
      eventAction = 'failure-http';
      eventLabel = '${err.message}, device port = $assumedDevicePort';
    } else {
      eventAction = 'failure-other';
      eventLabel = '$err, device port = $assumedDevicePort';
    }
182 183
    UsageEvent(
      _kEventName,
184 185
      eventAction,
      label: eventLabel,
186
      flutterUsage: _flutterUsage,
187
    ).send();
188 189
  }
}