attach.dart 17.7 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 '../artifacts.dart';
10
import '../base/common.dart';
11
import '../base/context.dart';
12
import '../base/file_system.dart';
13
import '../base/io.dart';
14
import '../base/utils.dart';
15
import '../cache.dart';
16
import '../commands/daemon.dart';
17
import '../compile.dart';
18
import '../device.dart';
19
import '../fuchsia/fuchsia_device.dart';
20
import '../globals.dart';
21 22
import '../ios/devices.dart';
import '../ios/simulators.dart';
23
import '../project.dart';
24 25
import '../protocol_discovery.dart';
import '../resident_runner.dart';
26
import '../run_cold.dart';
27 28 29 30 31 32 33 34 35
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:
/// ```
36 37 38 39 40 41
/// $ flutter attach --debug-uri http://127.0.0.1:12345/QqL7EFEDNG0=/
/// ```
///
/// If `--disable-service-auth-codes` was provided to the application at startup
/// time, a HotRunner can be attached with just a port:
/// ```
42 43 44 45 46 47 48 49 50 51
/// $ 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.
52 53 54
///
/// To attach to a flutter mod running on a fuchsia device, `--module` must
/// also be provided.
55
class AttachCommand extends FlutterCommand {
56
  AttachCommand({bool verboseHelp = false, this.hotRunnerFactory}) {
57
    addBuildModeFlags(defaultToRelease: false);
58
    usesIsolateFilterOption(hide: !verboseHelp);
59
    usesTargetOption();
60 61
    usesPortOptions();
    usesIpv6Flag();
62
    usesFilesystemOptions(hide: !verboseHelp);
63
    usesFuchsiaOptions(hide: !verboseHelp);
64 65
    argParser
      ..addOption(
66
        'debug-port',
67 68 69 70 71 72 73 74
        hide: !verboseHelp,
        help: 'Device port where the observatory is listening. Requires '
        '--disable-service-auth-codes to also be provided to the Flutter '
        'application at launch, otherwise this command will fail to connect to '
        'the application. In general, --debug-uri should be used instead.',
      )..addOption(
        'debug-uri',
        help: 'The URI at which the observatory is listening.',
75 76 77 78 79 80 81 82 83 84
      )..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',
85 86 87
        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.',
88 89 90 91
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
92 93 94 95
      )..addFlag('track-widget-creation',
        hide: !verboseHelp,
        help: 'Track widget creation locations.',
        defaultsTo: false,
96
      )..addFlag('machine',
97 98 99 100
        hide: !verboseHelp,
        negatable: false,
        help: 'Handle machine structured JSON command input and provide output '
              'and progress in machine friendly format.',
101
      );
102
    hotRunnerFactory ??= HotRunnerFactory();
103 104
  }

105 106
  HotRunnerFactory hotRunnerFactory;

107 108 109 110 111 112
  @override
  final String name = 'attach';

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

113
  int get debugPort {
114 115 116 117 118 119 120 121 122 123
    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;
  }

124 125 126 127 128 129 130 131 132 133 134
  Uri get debugUri {
    if (argResults['debug-uri'] == null) {
      return null;
    }
    final Uri uri = Uri.parse(argResults['debug-uri']);
    if (!uri.hasPort) {
      throwToolExit('Port not specified for `--debug-uri`: $uri');
    }
    return uri;
  }

135 136 137 138
  String get appId {
    return argResults['app-id'];
  }

139
  @override
140
  Future<void> validateCommand() async {
141
    await super.validateCommand();
142 143
    if (await findTargetDevice() == null)
      throwToolExit(null);
144
    debugPort;
145
    if (debugPort == null && debugUri == null && argResults.wasParsed(FlutterCommand.ipv6Flag)) {
146
      throwToolExit(
147
        'When the --debug-port or --debug-uri is unknown, this command determines '
148 149 150
        'the value of --ipv6 on its own.',
      );
    }
151
    if (debugPort == null && debugUri == null && argResults.wasParsed(FlutterCommand.observatoryPortOption)) {
152
      throwToolExit(
153
        'When the --debug-port or --debug-uri is unknown, this command does not use '
154 155 156
        'the value of --observatory-port.',
      );
    }
157 158 159 160
    if (debugPort != null && debugUri != null) {
      throwToolExit(
        'Either --debugPort or --debugUri can be provided, not both.');
    }
161 162
  }

163
  @override
164
  Future<FlutterCommandResult> runCommand() async {
165 166 167 168
    Cache.releaseLockEarly();

    await _validateArguments();

169 170
    writePidFile(argResults['pid-file']);

171
    final Device device = await findTargetDevice();
172 173 174 175 176 177 178 179 180 181 182 183 184

    final Artifacts artifacts = device.artifactOverrides ?? Artifacts.instance;
    await context.run<void>(
      body: () => _attachToDevice(device),
      overrides: <Type, Generator>{
        Artifacts: () => artifacts,
    });

    return null;
  }

  Future<void> _attachToDevice(Device device) async {
    final FlutterProject flutterProject = FlutterProject.current();
185 186 187 188 189 190 191 192 193 194 195 196
    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 null;
    }
    final int devicePort = await getDevicePort();
197 198

    final Daemon daemon = argResults['machine']
199 200
      ? Daemon(stdinCommandStream, stdoutCommandResponse,
            notifyingLogger: NotifyingLogger(), logToStdout: true)
201 202
      : null;

203
    Uri observatoryUri;
204 205 206 207 208
    bool usesIpv6 = ipv6;
    final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
    final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
    final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;

209
    bool attachLogger = false;
210
    if (devicePort == null  && debugUri == null) {
211 212 213
      if (device is FuchsiaDevice) {
        attachLogger = true;
        final String module = argResults['module'];
214 215
        if (module == null)
          throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
216 217
        usesIpv6 = device.ipv6;
        FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
218
        try {
219 220
          isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
          observatoryUri = await isolateDiscoveryProtocol.uri;
221
          printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
222
        } catch (_) {
223
          isolateDiscoveryProtocol?.dispose();
224 225 226 227
          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
          for (ForwardedPort port in ports) {
            await device.portForwarder.unforward(port);
          }
228
          rethrow;
229
        }
230 231
      } else if ((device is IOSDevice) || (device is IOSSimulator)) {
        final MDnsObservatoryDiscoveryResult result = await MDnsObservatoryDiscovery().query(applicationId: appId);
232 233 234
        if (result != null) {
          observatoryUri = await _buildObservatoryUri(device, hostname, result.port, result.authCode);
        }
235 236 237
      }
      // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
      if (observatoryUri == null) {
238 239 240 241 242 243 244 245
        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;
246 247
          // Determine ipv6 status from the scanned logs.
          usesIpv6 = observatoryDiscovery.ipv6;
248
          printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
249 250 251
        } finally {
          await observatoryDiscovery?.cancel();
        }
252 253
      }
    } else {
254
      observatoryUri = await _buildObservatoryUri(device,
255
          debugUri?.host ?? hostname, devicePort ?? debugUri.port, debugUri?.path);
256 257
    }
    try {
258
      final bool useHot = getBuildInfo().isDebug;
259
      final FlutterDevice flutterDevice = await FlutterDevice.create(
260
        device,
261
        flutterProject: flutterProject,
262
        trackWidgetCreation: argResults['track-widget-creation'],
263 264 265
        dillOutputPath: argResults['output-dill'],
        fileSystemRoots: argResults['filesystem-root'],
        fileSystemScheme: argResults['filesystem-scheme'],
266
        viewFilter: argResults['isolate-filter'],
267
        target: argResults['target'],
268
        targetModel: TargetModel(argResults['target-model']),
269
        buildMode: getBuildMode(),
270
      );
271
      flutterDevice.observatoryUris = <Uri>[ observatoryUri ];
272 273 274 275 276 277 278 279 280 281 282 283
      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,
284
            flutterProject: flutterProject,
285 286 287 288 289 290 291
          )
        : ColdRunner(
            flutterDevices,
            target: targetFile,
            debuggingOptions: debuggingOptions,
            ipv6: usesIpv6,
          );
292 293 294
      if (attachLogger) {
        flutterDevice.startEchoingDeviceLog();
      }
295 296

      int result;
297 298 299
      if (daemon != null) {
        AppInstance app;
        try {
300 301 302 303 304 305 306
          app = await daemon.appDomain.launch(
            runner,
            runner.attach,
            device,
            null,
            true,
            fs.currentDirectory,
307
            LaunchMode.attach,
308
          );
309 310 311
        } catch (error) {
          throwToolExit(error.toString());
        }
312 313
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
314
      } else {
315 316
        result = await runner.attach();
        assert(result != null);
317
      }
318 319
      if (result != 0)
        throwToolExit(null, exitCode: result);
320
    } finally {
321
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
322 323 324
      for (ForwardedPort port in ports) {
        await device.portForwarder.unforward(port);
      }
325 326 327
    }
  }

328
  Future<void> _validateArguments() async { }
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344

  Future<Uri> _buildObservatoryUri(Device device,
      String host, int devicePort, [String authCode]) async {
    String path = '/';
    if (authCode != null) {
      path = authCode;
    }
    // Not having a trailing slash can cause problems in some situations.
    // Ensure that there's one present.
    if (!path.endsWith('/')) {
      path += '/';
    }
    final int localPort = observatoryPort
        ?? await device.portForwarder.forward(devicePort);
    return Uri(scheme: 'http', host: host, port: localPort, path: path);
  }
345
}
346 347

class HotRunnerFactory {
348 349 350 351 352 353 354 355 356 357 358 359 360
  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,
361
    FlutterProject flutterProject,
362
  }) => HotRunner(
363 364 365 366 367 368 369 370 371 372 373 374 375
    devices,
    target: target,
    debuggingOptions: debuggingOptions,
    usesTerminalUI: usesTerminalUI,
    benchmarkMode: benchmarkMode,
    applicationBinary: applicationBinary,
    hostIsIde: hostIsIde,
    projectRootPath: projectRootPath,
    packagesFilePath: packagesFilePath,
    dillOutputPath: dillOutputPath,
    stayResident: stayResident,
    ipv6: ipv6,
  );
376
}
377

378 379 380 381 382 383 384 385 386
class MDnsObservatoryDiscoveryResult {
  MDnsObservatoryDiscoveryResult(this.port, this.authCode);
  final int port;
  final String authCode;
}

/// A wrapper around [MDnsClient] to find a Dart observatory instance.
class MDnsObservatoryDiscovery {
  /// Creates a new [MDnsObservatoryDiscovery] object.
387 388 389 390 391
  ///
  /// 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.
392
  MDnsObservatoryDiscovery({MDnsClient mdnsClient})
393 394 395 396 397 398 399
    : client = mdnsClient ?? MDnsClient();

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

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

400
  /// Executes an mDNS query for a Dart Observatory.
401 402 403 404 405
  ///
  /// 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.
  ///
406 407
  /// If it is not null, this method will find the port and authentication code
  /// of the Dart Observatory for that application. If it cannot find a Dart
408 409 410 411 412 413 414
  /// 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.
  ///
415 416 417 418
  /// If it is null and there is only one available instance of Observatory,
  /// it will return that instance's information regardless of what application
  /// the Observatory instance is for.
  Future<MDnsObservatoryDiscoveryResult> query({String applicationId}) async {
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
    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('');
453
         for (final String uniqueDomainName in uniqueDomainNames) {
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
          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}).');
      }
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
      printStatus('Checking for authentication code for $domainName');
      final List<TxtResourceRecord> txt = await client
        .lookup<TxtResourceRecord>(
            ResourceRecordQuery.text(domainName),
        )
        ?.toList();
      if (txt == null || txt.isEmpty) {
        return MDnsObservatoryDiscoveryResult(srv.first.port, '');
      }
      String authCode = '';
      const String authCodePrefix = 'authCode=';
      String raw = txt.first.text;
      // TXT has a format of [<length byte>, text], so if the length is 2,
      // that means that TXT is empty.
      if (raw.length > 2) {
        // Remove length byte from raw txt.
        raw = raw.substring(1);
        if (raw.startsWith(authCodePrefix)) {
          authCode = raw.substring(authCodePrefix.length);
          // The Observatory currently expects a trailing '/' as part of the
          // URI, otherwise an invalid authentication code response is given.
          if (!authCode.endsWith('/')) {
            authCode += '/';
          }
        }
      }
      return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
501 502 503 504 505
    } finally {
      client.stop();
    }
  }
}