attach.dart 14.9 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 '../commands/daemon.dart';
16
import '../compile.dart';
17
import '../device.dart';
18
import '../fuchsia/fuchsia_device.dart';
19
import '../globals.dart' as globals;
20 21
import '../ios/devices.dart';
import '../ios/simulators.dart';
22
import '../mdns_discovery.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
    usesTargetOption();
59 60
    usesPortOptions();
    usesIpv6Flag();
61
    usesFilesystemOptions(hide: !verboseHelp);
62
    usesFuchsiaOptions(hide: !verboseHelp);
63
    usesDartDefineOption();
64
    usesDeviceUserOption();
65 66
    addEnableExperimentation(hide: !verboseHelp);
    addNullSafetyModeOptions(hide: !verboseHelp);
67 68
    argParser
      ..addOption(
69
        'debug-port',
70 71 72 73 74 75 76 77
        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.',
78 79
      )..addOption(
        'app-id',
80
        help: 'The package name (Android) or bundle identifier (iOS) for the app. '
81 82 83 84 85 86 87
              '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',
88 89 90
        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.',
91 92 93 94
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
95
      )..addFlag('machine',
96 97 98 99
        hide: !verboseHelp,
        negatable: false,
        help: 'Handle machine structured JSON command input and provide output '
              'and progress in machine friendly format.',
100
      );
101
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
102
    addDdsOptions(verboseHelp: verboseHelp);
103
    usesDeviceTimeoutOption();
104
    hotRunnerFactory ??= HotRunnerFactory();
105 106
  }

107 108
  HotRunnerFactory hotRunnerFactory;

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

  @override
113
  final String description = '''Attach to a running app.
114 115 116 117 118 119 120 121 122 123 124 125 126

  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.

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

  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.
  `\$ flutter attach --debug-port 12345`''';
127

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

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

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

158 159
  String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);

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

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

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

196
    writePidFile(stringArg('pid-file'));
197

198
    final Device device = await findTargetDevice();
199

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

207
    return FlutterCommandResult.success();
208 209 210 211
  }

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

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

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

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

303
    globals.terminal.usesTerminalUi = daemon == null;
304

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

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
  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);

    final FlutterDevice flutterDevice = await FlutterDevice.create(
      device,
      flutterProject: flutterProject,
      fileSystemRoots: stringsArg('filesystem-root'),
      fileSystemScheme: stringArg('filesystem-scheme'),
      target: stringArg('target'),
      targetModel: TargetModel(stringArg('target-model')),
391
      buildInfo: getBuildInfo(),
392
      userIdentifier: userIdentifier,
393
      platform: globals.platform,
394 395 396
    );
    flutterDevice.observatoryUris = observatoryUris;
    final List<FlutterDevice> flutterDevices =  <FlutterDevice>[flutterDevice];
397
    final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo(), disableDds: boolArg('disable-dds'));
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417

    return getBuildInfo().isDebug
      ? 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,
        );
  }

418
  Future<void> _validateArguments() async { }
419
}
420 421

class HotRunnerFactory {
422 423 424 425 426 427 428 429 430 431 432 433
  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,
434
    FlutterProject flutterProject,
435
  }) => HotRunner(
436 437 438 439 440 441 442 443 444 445 446
    devices,
    target: target,
    debuggingOptions: debuggingOptions,
    benchmarkMode: benchmarkMode,
    applicationBinary: applicationBinary,
    hostIsIde: hostIsIde,
    projectRootPath: projectRootPath,
    dillOutputPath: dillOutputPath,
    stayResident: stayResident,
    ipv6: ipv6,
  );
447
}