attach.dart 13.9 KB
Newer Older
1 2 3 4 5 6
// Copyright 2018 The Chromium 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 8
import 'package:multicast_dns/multicast_dns.dart';

9
import '../base/common.dart';
10
import '../base/file_system.dart';
11
import '../base/io.dart';
12
import '../base/utils.dart';
13
import '../cache.dart';
14
import '../commands/daemon.dart';
15
import '../compile.dart';
16
import '../device.dart';
17
import '../fuchsia/fuchsia_device.dart';
18
import '../globals.dart';
19 20
import '../ios/devices.dart';
import '../ios/simulators.dart';
21
import '../project.dart';
22 23
import '../protocol_discovery.dart';
import '../resident_runner.dart';
24
import '../run_cold.dart';
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
import '../run_hot.dart';
import '../runner/flutter_command.dart';

/// A Flutter-command that attaches to applications that have been launched
/// without `flutter run`.
///
/// With an application already running, a HotRunner can be attached to it
/// with:
/// ```
/// $ flutter attach --debug-port 12345
/// ```
///
/// Alternatively, the attach command can start listening and scan for new
/// programs that become active:
/// ```
/// $ flutter attach
/// ```
/// As soon as a new observatory is detected the command attaches to it and
/// enables hot reloading.
44 45 46
///
/// To attach to a flutter mod running on a fuchsia device, `--module` must
/// also be provided.
47
class AttachCommand extends FlutterCommand {
48
  AttachCommand({bool verboseHelp = false, this.hotRunnerFactory}) {
49
    addBuildModeFlags(defaultToRelease: false);
50
    usesIsolateFilterOption(hide: !verboseHelp);
51
    usesTargetOption();
52 53
    usesPortOptions();
    usesIpv6Flag();
54
    usesFilesystemOptions(hide: !verboseHelp);
55
    usesFuchsiaOptions(hide: !verboseHelp);
56 57
    argParser
      ..addOption(
58
        'debug-port',
59
        help: 'Device port where the observatory is listening.',
60 61 62 63 64 65 66 67 68 69
      )..addOption(
        'app-id',
        help: 'The package name (Android) or bundle identifier (iOS) for the application. '
              'This can be specified to avoid being prompted if multiple observatory ports '
              'are advertised.\n'
              'If you have multiple devices or emulators running, you should include the '
              'device hostname as well, e.g. "com.example.myApp@my-iphone".\n'
              'This parameter is case-insensitive.',
      )..addOption(
        'pid-file',
70 71 72
        help: 'Specify a file to write the process id to. '
              'You can send SIGUSR1 to trigger a hot reload '
              'and SIGUSR2 to trigger a hot restart.',
73 74 75 76
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
77
      )..addFlag('machine',
78 79 80 81
        hide: !verboseHelp,
        negatable: false,
        help: 'Handle machine structured JSON command input and provide output '
              'and progress in machine friendly format.',
82
      );
83
    hotRunnerFactory ??= HotRunnerFactory();
84 85
  }

86 87
  HotRunnerFactory hotRunnerFactory;

88 89 90 91 92 93
  @override
  final String name = 'attach';

  @override
  final String description = 'Attach to a running application.';

94
  int get debugPort {
95 96 97 98 99 100 101 102 103 104
    if (argResults['debug-port'] == null)
      return null;
    try {
      return int.parse(argResults['debug-port']);
    } catch (error) {
      throwToolExit('Invalid port for `--debug-port`: $error');
    }
    return null;
  }

105 106 107 108
  String get appId {
    return argResults['app-id'];
  }

109
  @override
110
  Future<void> validateCommand() async {
111
    await super.validateCommand();
112 113
    if (await findTargetDevice() == null)
      throwToolExit(null);
114 115 116 117 118 119 120 121 122 123 124 125 126
    debugPort;
    if (debugPort == null && argResults.wasParsed(FlutterCommand.ipv6Flag)) {
      throwToolExit(
        'When the --debug-port is unknown, this command determines '
        'the value of --ipv6 on its own.',
      );
    }
    if (debugPort == null && argResults.wasParsed(FlutterCommand.observatoryPortOption)) {
      throwToolExit(
        'When the --debug-port is unknown, this command does not use '
        'the value of --observatory-port.',
      );
    }
127 128
  }

129
  @override
130
  Future<FlutterCommandResult> runCommand() async {
131 132
    final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
    final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
133
    final FlutterProject flutterProject = await FlutterProject.current();
134

135 136 137 138
    Cache.releaseLockEarly();

    await _validateArguments();

139 140
    writePidFile(argResults['pid-file']);

141
    final Device device = await findTargetDevice();
142 143 144 145 146 147 148 149 150 151 152 153 154
    Future<int> getDevicePort() async {
      if (debugPort != null) {
        return debugPort;
      }
      // This call takes a non-trivial amount of time, and only iOS devices and
      // simulators support it.
      // If/when we do this on Android or other platforms, we can update it here.
      if (device is IOSDevice || device is IOSSimulator) {
        return MDnsObservatoryPortDiscovery().queryForPort(applicationId: appId);
      }
      return null;
    }
    final int devicePort = await getDevicePort();
155 156

    final Daemon daemon = argResults['machine']
157 158
      ? Daemon(stdinCommandStream, stdoutCommandResponse,
            notifyingLogger: NotifyingLogger(), logToStdout: true)
159 160
      : null;

161
    Uri observatoryUri;
162
    bool usesIpv6 = false;
163
    bool attachLogger = false;
164
    if (devicePort == null) {
165 166 167
      if (device is FuchsiaDevice) {
        attachLogger = true;
        final String module = argResults['module'];
168 169
        if (module == null)
          throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
170 171
        usesIpv6 = device.ipv6;
        FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
172
        try {
173 174
          isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
          observatoryUri = await isolateDiscoveryProtocol.uri;
175
          printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
176
        } catch (_) {
177
          isolateDiscoveryProtocol?.dispose();
178 179 180 181
          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
          for (ForwardedPort port in ports) {
            await device.portForwarder.unforward(port);
          }
182
          rethrow;
183 184 185 186 187 188 189 190 191 192
        }
      } else {
        ProtocolDiscovery observatoryDiscovery;
        try {
          observatoryDiscovery = ProtocolDiscovery.observatory(
            device.getLogReader(),
            portForwarder: device.portForwarder,
          );
          printStatus('Waiting for a connection from Flutter on ${device.name}...');
          observatoryUri = await observatoryDiscovery.uri;
193 194
          // Determine ipv6 status from the scanned logs.
          usesIpv6 = observatoryDiscovery.ipv6;
195
          printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
196 197 198
        } finally {
          await observatoryDiscovery?.cancel();
        }
199 200
      }
    } else {
201 202 203 204 205 206
      usesIpv6 = ipv6;
      final int localPort = observatoryPort
        ?? await device.portForwarder.forward(devicePort);
      observatoryUri = usesIpv6
        ? Uri.parse('http://[$ipv6Loopback]:$localPort/')
        : Uri.parse('http://$ipv4Loopback:$localPort/');
207 208
    }
    try {
209
      final bool useHot = getBuildInfo().isDebug;
210
      final FlutterDevice flutterDevice = await FlutterDevice.create(
211 212 213 214 215
        device,
        trackWidgetCreation: false,
        dillOutputPath: argResults['output-dill'],
        fileSystemRoots: argResults['filesystem-root'],
        fileSystemScheme: argResults['filesystem-scheme'],
216
        viewFilter: argResults['isolate-filter'],
217
        target: argResults['target'],
218
        targetModel: TargetModel(argResults['target-model']),
219
      );
220
      flutterDevice.observatoryUris = <Uri>[ observatoryUri ];
221 222 223 224 225 226 227 228 229 230 231 232
      final List<FlutterDevice> flutterDevices =  <FlutterDevice>[flutterDevice];
      final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo());
      final ResidentRunner runner = useHot ?
          hotRunnerFactory.build(
            flutterDevices,
            target: targetFile,
            debuggingOptions: debuggingOptions,
            packagesFilePath: globalResults['packages'],
            usesTerminalUI: daemon == null,
            projectRootPath: argResults['project-root'],
            dillOutputPath: argResults['output-dill'],
            ipv6: usesIpv6,
233
            flutterProject: flutterProject,
234 235 236 237 238 239 240
          )
        : ColdRunner(
            flutterDevices,
            target: targetFile,
            debuggingOptions: debuggingOptions,
            ipv6: usesIpv6,
          );
241 242 243
      if (attachLogger) {
        flutterDevice.startEchoingDeviceLog();
      }
244 245

      int result;
246 247 248
      if (daemon != null) {
        AppInstance app;
        try {
249 250 251 252 253 254 255
          app = await daemon.appDomain.launch(
            runner,
            runner.attach,
            device,
            null,
            true,
            fs.currentDirectory,
256
            LaunchMode.attach,
257
          );
258 259 260
        } catch (error) {
          throwToolExit(error.toString());
        }
261 262
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
263
      } else {
264 265
        result = await runner.attach();
        assert(result != null);
266
      }
267 268
      if (result != 0)
        throwToolExit(null, exitCode: result);
269
    } finally {
270
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
271 272 273
      for (ForwardedPort port in ports) {
        await device.portForwarder.unforward(port);
      }
274
    }
275
    return null;
276 277
  }

278
  Future<void> _validateArguments() async { }
279
}
280 281

class HotRunnerFactory {
282 283 284 285 286 287 288 289 290 291 292 293 294
  HotRunner build(
    List<FlutterDevice> devices, {
    String target,
    DebuggingOptions debuggingOptions,
    bool usesTerminalUI = true,
    bool benchmarkMode = false,
    File applicationBinary,
    bool hostIsIde = false,
    String projectRootPath,
    String packagesFilePath,
    String dillOutputPath,
    bool stayResident = true,
    bool ipv6 = false,
295
    FlutterProject flutterProject,
296
  }) => HotRunner(
297 298 299 300 301 302 303 304 305 306 307 308 309
    devices,
    target: target,
    debuggingOptions: debuggingOptions,
    usesTerminalUI: usesTerminalUI,
    benchmarkMode: benchmarkMode,
    applicationBinary: applicationBinary,
    hostIsIde: hostIsIde,
    projectRootPath: projectRootPath,
    packagesFilePath: packagesFilePath,
    dillOutputPath: dillOutputPath,
    stayResident: stayResident,
    ipv6: ipv6,
  );
310
}
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406

/// A wrapper around [MDnsClient] to find a Dart observatory port.
class MDnsObservatoryPortDiscovery {
  /// Creates a new [MDnsObservatoryPortDiscovery] object.
  ///
  /// The [client] parameter will be defaulted to a new [MDnsClient] if null.
  /// The [applicationId] parameter may be null, and can be used to
  /// automatically select which application to use if multiple are advertising
  /// Dart observatory ports.
  MDnsObservatoryPortDiscovery({MDnsClient mdnsClient})
    : client = mdnsClient ?? MDnsClient();

  /// The [MDnsClient] used to do a lookup.
  final MDnsClient client;

  static const String dartObservatoryName = '_dartobservatory._tcp.local';

  /// Executes an mDNS query for a Dart Observatory port.
  ///
  /// The [applicationId] parameter may be used to specify which application
  /// to find.  For Android, it refers to the package name; on iOS, it refers to
  /// the bundle ID.
  ///
  /// If it is not null, this method will find the port of the
  /// Dart Observatory for that application. If it cannot find a Dart
  /// Observatory matching that application identifier, it will call
  /// [throwToolExit].
  ///
  /// If it is null and there are multiple ports available, the user will be
  /// prompted with a list of available observatory ports and asked to select
  /// one.
  ///
  /// If it is null and there is only one available port, it will return that
  /// port regardless of what application the port is for.
  Future<int> queryForPort({String applicationId}) async {
    printStatus('Checking for advertised Dart observatories...');
    try {
      await client.start();
      final List<PtrResourceRecord> pointerRecords = await client
          .lookup<PtrResourceRecord>(
            ResourceRecordQuery.serverPointer(dartObservatoryName),
          )
          .toList();
      if (pointerRecords.isEmpty) {
        return null;
      }
      // We have no guarantee that we won't get multiple hits from the same
      // service on this.
      final List<String> uniqueDomainNames = pointerRecords
          .map<String>((PtrResourceRecord record) => record.domainName)
          .toSet()
          .toList();

      String domainName;
      if (applicationId != null) {
        for (String name in uniqueDomainNames) {
          if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
            domainName = name;
            break;
          }
        }
        if (domainName == null) {
          throwToolExit('Did not find a observatory port advertised for $applicationId.');
        }
      } else if (uniqueDomainNames.length > 1) {
        final StringBuffer buffer = StringBuffer();
        buffer.writeln('There are multiple observatory ports available.');
        buffer.writeln('Rerun this command with one of the following passed in as the appId:');
        buffer.writeln('');
        for (final String uniqueDomainName in uniqueDomainNames) {
          buffer.writeln('  flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
        }
        throwToolExit(buffer.toString());
      } else {
        domainName = pointerRecords[0].domainName;
      }
      printStatus('Checking for available port on $domainName');
      // Here, if we get more than one, it should just be a duplicate.
      final List<SrvResourceRecord> srv = await client
          .lookup<SrvResourceRecord>(
            ResourceRecordQuery.service(domainName),
          )
          .toList();
      if (srv.isEmpty) {
        return null;
      }
      if (srv.length > 1) {
        printError('Unexpectedly found more than one observatory report for $domainName '
                   '- using first one (${srv.first.port}).');
      }
      return srv.first.port;
    } finally {
      client.stop();
    }
  }
}