attach.dart 14.8 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 '../features.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
import '../run_hot.dart';
import '../runner/flutter_command.dart';
30
import '../widget_cache.dart';
31 32 33 34 35 36 37

/// 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:
/// ```
38 39 40 41 42 43
/// $ 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:
/// ```
44 45 46 47 48 49 50 51 52 53
/// $ 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.
54 55 56
///
/// To attach to a flutter mod running on a fuchsia device, `--module` must
/// also be provided.
57
class AttachCommand extends FlutterCommand {
58
  AttachCommand({bool verboseHelp = false, this.hotRunnerFactory}) {
59
    addBuildModeFlags(defaultToRelease: false);
60
    usesTargetOption();
61 62
    usesPortOptions();
    usesIpv6Flag();
63
    usesFilesystemOptions(hide: !verboseHelp);
64
    usesFuchsiaOptions(hide: !verboseHelp);
65
    usesDartDefineOption();
66
    usesDeviceUserOption();
67 68
    addEnableExperimentation(hide: !verboseHelp);
    addNullSafetyModeOptions(hide: !verboseHelp);
69 70
    argParser
      ..addOption(
71
        'debug-port',
72 73 74 75 76 77 78 79
        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.',
80 81 82 83 84 85 86 87 88 89
      )..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',
90 91 92
        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.',
93 94 95 96
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
97
      )..addFlag('machine',
98 99 100 101
        hide: !verboseHelp,
        negatable: false,
        help: 'Handle machine structured JSON command input and provide output '
              'and progress in machine friendly format.',
102
      );
103
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
104
    addDdsOptions(verboseHelp: verboseHelp);
105
    usesDeviceTimeoutOption();
106
    hotRunnerFactory ??= HotRunnerFactory();
107 108
  }

109 110
  HotRunnerFactory hotRunnerFactory;

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

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

  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`''';
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
    final Uri uri = Uri.parse(stringArg('debug-uri'));
147 148 149 150 151 152
    if (!uri.hasPort) {
      throwToolExit('Port not specified for `--debug-uri`: $uri');
    }
    return uri;
  }

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

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

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

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

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

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

197
    final Device device = await findTargetDevice();
198

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

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

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

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

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

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

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

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

367 368 369 370 371 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);

    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')),
385
      buildInfo: getBuildInfo(),
386
      userIdentifier: userIdentifier,
387
      widgetCache: WidgetCache(featureFlags: featureFlags),
388 389 390
    );
    flutterDevice.observatoryUris = observatoryUris;
    final List<FlutterDevice> flutterDevices =  <FlutterDevice>[flutterDevice];
391
    final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo(), disableDds: boolArg('disable-dds'));
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411

    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,
        );
  }

412
  Future<void> _validateArguments() async { }
413
}
414 415

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