attach.dart 15 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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:meta/meta.dart';

9
import '../android/android_device.dart';
10
import '../artifacts.dart';
11
import '../base/common.dart';
12
import '../base/context.dart';
13
import '../base/file_system.dart';
14
import '../base/io.dart';
15
import  '../build_info.dart';
16
import '../commands/daemon.dart';
17
import '../compile.dart';
18
import '../device.dart';
19
import '../fuchsia/fuchsia_device.dart';
20
import '../globals.dart' as globals;
21 22
import '../ios/devices.dart';
import '../ios/simulators.dart';
23
import '../mdns_discovery.dart';
24
import '../project.dart';
25 26
import '../protocol_discovery.dart';
import '../resident_runner.dart';
27
import '../run_cold.dart';
28 29 30 31 32 33 34 35 36
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:
/// ```
37 38 39 40 41 42
/// $ 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:
/// ```
43 44 45 46 47 48 49 50 51 52
/// $ 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.
53 54 55
///
/// To attach to a flutter mod running on a fuchsia device, `--module` must
/// also be provided.
56
class AttachCommand extends FlutterCommand {
57
  AttachCommand({bool verboseHelp = false, this.hotRunnerFactory}) {
58
    addBuildModeFlags(defaultToRelease: false, excludeRelease: true);
59
    usesTargetOption();
60 61
    usesPortOptions();
    usesIpv6Flag();
62
    usesFilesystemOptions(hide: !verboseHelp);
63
    usesFuchsiaOptions(hide: !verboseHelp);
64
    usesDartDefineOption();
65
    usesDeviceUserOption();
66 67
    addEnableExperimentation(hide: !verboseHelp);
    addNullSafetyModeOptions(hide: !verboseHelp);
68 69
    argParser
      ..addOption(
70
        'debug-port',
71 72 73 74 75 76 77 78
        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.',
79 80
      )..addOption(
        'app-id',
81
        help: 'The package name (Android) or bundle identifier (iOS) for the app. '
82 83 84 85 86 87 88
              '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',
89 90 91
        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.',
92 93 94 95
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
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
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
103
    addDdsOptions(verboseHelp: verboseHelp);
104
    usesDeviceTimeoutOption();
105
    hotRunnerFactory ??= HotRunnerFactory();
106 107
  }

108 109
  HotRunnerFactory hotRunnerFactory;

110 111 112 113
  @override
  final String name = 'attach';

  @override
114
  final String description = r'''
115
Attach to a running app.
116

117 118 119 120
For attaching to Android or iOS devices, simply using `flutter attach` is
usually sufficient. The tool will search for a running Flutter app or module,
if available. Otherwise, the tool will wait for the next Flutter app or module
to launch before attaching.
121

122
For Fuchsia, the module name must be provided, e.g. `$flutter attach
123 124
--module=mod_name`. This can be called either before or after the application
is started.
125

126 127
If the app or module is already running and the specific observatory port is
known, it can be explicitly provided to attach via the command-line, e.g.
128
`$ flutter attach --debug-port 12345`''';
129

130
  int get debugPort {
131
    if (argResults['debug-port'] == null) {
132
      return null;
133
    }
134
    try {
135
      return int.parse(stringArg('debug-port'));
136
    } on Exception catch (error) {
137 138 139 140 141
      throwToolExit('Invalid port for `--debug-port`: $error');
    }
    return null;
  }

142 143 144 145
  Uri get debugUri {
    if (argResults['debug-uri'] == null) {
      return null;
    }
146 147 148 149
    final Uri uri = Uri.tryParse(stringArg('debug-uri'));
    if (uri == null) {
      throwToolExit('Invalid `--debug-uri`: ${stringArg('debug-uri')}');
    }
150 151 152 153 154 155
    if (!uri.hasPort) {
      throwToolExit('Port not specified for `--debug-uri`: $uri');
    }
    return uri;
  }

156
  String get appId {
157
    return stringArg('app-id');
158 159
  }

160 161
  String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);

162
  @override
163
  Future<void> validateCommand() async {
164
    await super.validateCommand();
165
    if (await findTargetDevice() == null) {
166
      throwToolExit(null);
167
    }
168
    debugPort;
169
    if (debugPort == null && debugUri == null && argResults.wasParsed(FlutterCommand.ipv6Flag)) {
170
      throwToolExit(
171
        'When the --debug-port or --debug-uri is unknown, this command determines '
172 173 174
        'the value of --ipv6 on its own.',
      );
    }
175
    if (debugPort == null && debugUri == null && argResults.wasParsed(FlutterCommand.observatoryPortOption)) {
176
      throwToolExit(
177
        'When the --debug-port or --debug-uri is unknown, this command does not use '
178 179 180
        'the value of --observatory-port.',
      );
    }
181 182 183 184
    if (debugPort != null && debugUri != null) {
      throwToolExit(
        'Either --debugPort or --debugUri can be provided, not both.');
    }
185 186 187 188 189 190 191

    if (userIdentifier != null) {
      final Device device = await findTargetDevice();
      if (device is! AndroidDevice) {
        throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
      }
    }
192 193
  }

194
  @override
195
  Future<FlutterCommandResult> runCommand() async {
196 197
    await _validateArguments();

198
    writePidFile(stringArg('pid-file'));
199

200
    final Device device = await findTargetDevice();
201

202
    final Artifacts overrideArtifacts = device.artifactOverrides ?? globals.artifacts;
203 204 205
    await context.run<void>(
      body: () => _attachToDevice(device),
      overrides: <Type, Generator>{
206
        Artifacts: () => overrideArtifacts,
207 208
    });

209
    return FlutterCommandResult.success();
210 211 212 213
  }

  Future<void> _attachToDevice(Device device) async {
    final FlutterProject flutterProject = FlutterProject.current();
214 215 216 217 218 219 220 221 222 223 224 225
    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();
226

227
    final Daemon daemon = boolArg('machine')
228 229 230
      ? Daemon(
          stdinCommandStream,
          stdoutCommandResponse,
231 232
          notifyingLogger: (globals.logger is NotifyingLogger)
            ? globals.logger as NotifyingLogger
233
            : NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
234 235
          logToStdout: true,
        )
236 237
      : null;

238
    Stream<Uri> observatoryUri;
239 240 241 242 243
    bool usesIpv6 = ipv6;
    final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
    final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
    final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;

244
    if (devicePort == null && debugUri == null) {
245
      if (device is FuchsiaDevice) {
246
        final String module = stringArg('module');
247
        if (module == null) {
248
          throwToolExit("'--module' is required for attaching to a Fuchsia device");
249
        }
250
        usesIpv6 = device.ipv6;
251
        FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
252
        try {
253
          isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
254
          observatoryUri = Stream<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
255
        } on Exception {
256
          isolateDiscoveryProtocol?.dispose();
257
          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
258
          for (final ForwardedPort port in ports) {
259 260
            await device.portForwarder.unforward(port);
          }
261
          rethrow;
262
        }
263
      } else if ((device is IOSDevice) || (device is IOSSimulator)) {
264 265 266 267 268 269 270 271 272 273
        final Uri uriFromMdns =
          await MDnsObservatoryDiscovery.instance.getObservatoryUri(
            appId,
            device,
            usesIpv6: usesIpv6,
            deviceVmservicePort: deviceVmservicePort,
          );
        observatoryUri = uriFromMdns == null
          ? null
          : Stream<Uri>.value(uriFromMdns).asBroadcastStream();
274 275 276
      }
      // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
      if (observatoryUri == null) {
277 278
        final ProtocolDiscovery observatoryDiscovery =
          ProtocolDiscovery.observatory(
279 280 281
            // If it's an Android device, attaching relies on past log searching
            // to find the service protocol.
            await device.getLogReader(includePastLogs: device is AndroidDevice),
282
            portForwarder: device.portForwarder,
283 284 285
            ipv6: ipv6,
            devicePort: deviceVmservicePort,
            hostPort: hostVmservicePort,
286
          );
287
        globals.printStatus('Waiting for a connection from Flutter on ${device.name}...');
288 289 290
        observatoryUri = observatoryDiscovery.uris;
        // Determine ipv6 status from the scanned logs.
        usesIpv6 = observatoryDiscovery.ipv6;
291 292
      }
    } else {
293 294 295 296 297 298 299 300
      observatoryUri = Stream<Uri>
        .fromFuture(
          buildObservatoryUri(
            device,
            debugUri?.host ?? hostname,
            devicePort ?? debugUri.port,
            hostVmservicePort,
            debugUri?.path,
301
          )
302 303 304
        ).asBroadcastStream();
    }

305
    globals.terminal.usesTerminalUi = daemon == null;
306

307
    try {
308
      int result;
309
      if (daemon != null) {
310 311 312 313 314 315
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
316 317
        AppInstance app;
        try {
318 319 320 321 322 323
          app = await daemon.appDomain.launch(
            runner,
            runner.attach,
            device,
            null,
            true,
324
            globals.fs.currentDirectory,
325
            LaunchMode.attach,
326
            globals.logger as AppRunLogger,
327
          );
328
        } on Exception catch (error) {
329 330
          throwToolExit(error.toString());
        }
331 332
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
333 334 335 336 337 338 339 340 341
        return;
      }
      while (true) {
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
342
        final Completer<void> onAppStart = Completer<void>.sync();
343
        TerminalHandler terminalHandler;
344
        unawaited(onAppStart.future.whenComplete(() {
345 346 347 348 349 350
          terminalHandler = TerminalHandler(
            runner,
            logger: globals.logger,
            terminal: globals.terminal,
            signals: globals.signals,
          )
351 352 353 354 355 356
            ..setupTerminal()
            ..registerSignalHandlers();
        }));
        result = await runner.attach(
          appStartedCompleter: onAppStart,
        );
357 358 359 360
        if (result != 0) {
          throwToolExit(null, exitCode: result);
        }
        terminalHandler?.stop();
361
        assert(result != null);
362 363 364
        if (runner.exited || !runner.isWaitingForObservatory) {
          break;
        }
365
        globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
366
      }
367
    } finally {
368
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
369
      for (final ForwardedPort port in ports) {
370 371
        await device.portForwarder.unforward(port);
      }
372 373 374
    }
  }

375 376 377 378 379 380 381 382 383 384
  Future<ResidentRunner> createResidentRunner({
    @required Stream<Uri> observatoryUris,
    @required Device device,
    @required FlutterProject flutterProject,
    @required bool usesIpv6,
  }) async {
    assert(observatoryUris != null);
    assert(device != null);
    assert(flutterProject != null);
    assert(usesIpv6 != null);
385
    final BuildInfo buildInfo = await getBuildInfo();
386 387 388 389 390 391 392

    final FlutterDevice flutterDevice = await FlutterDevice.create(
      device,
      fileSystemRoots: stringsArg('filesystem-root'),
      fileSystemScheme: stringArg('filesystem-scheme'),
      target: stringArg('target'),
      targetModel: TargetModel(stringArg('target-model')),
393
      buildInfo: buildInfo,
394
      userIdentifier: userIdentifier,
395
      platform: globals.platform,
396 397 398
    );
    flutterDevice.observatoryUris = observatoryUris;
    final List<FlutterDevice> flutterDevices =  <FlutterDevice>[flutterDevice];
399
    final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(buildInfo, disableDds: boolArg('disable-dds'));
400

401
    return buildInfo.isDebug
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
      ? hotRunnerFactory.build(
          flutterDevices,
          target: targetFile,
          debuggingOptions: debuggingOptions,
          packagesFilePath: globalResults['packages'] as String,
          projectRootPath: stringArg('project-root'),
          dillOutputPath: stringArg('output-dill'),
          ipv6: usesIpv6,
          flutterProject: flutterProject,
        )
      : ColdRunner(
          flutterDevices,
          target: targetFile,
          debuggingOptions: debuggingOptions,
          ipv6: usesIpv6,
        );
  }

420
  Future<void> _validateArguments() async { }
421
}
422 423

class HotRunnerFactory {
424 425 426 427 428 429 430 431 432 433 434 435
  HotRunner build(
    List<FlutterDevice> devices, {
    String target,
    DebuggingOptions debuggingOptions,
    bool benchmarkMode = false,
    File applicationBinary,
    bool hostIsIde = false,
    String projectRootPath,
    String packagesFilePath,
    String dillOutputPath,
    bool stayResident = true,
    bool ipv6 = false,
436
    FlutterProject flutterProject,
437
  }) => HotRunner(
438 439 440 441 442 443 444 445 446 447 448
    devices,
    target: target,
    debuggingOptions: debuggingOptions,
    benchmarkMode: benchmarkMode,
    applicationBinary: applicationBinary,
    hostIsIde: hostIsIde,
    projectRootPath: projectRootPath,
    dillOutputPath: dillOutputPath,
    stayResident: stayResident,
    ipv6: ipv6,
  );
449
}