attach.dart 13.4 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 '../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' 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);
59
    usesIsolateFilterOption(hide: !verboseHelp);
60
    usesTargetOption();
61 62
    usesPortOptions();
    usesIpv6Flag();
63
    usesFilesystemOptions(hide: !verboseHelp);
64
    usesFuchsiaOptions(hide: !verboseHelp);
65
    usesDartDefines();
66 67
    argParser
      ..addOption(
68
        'debug-port',
69 70 71 72 73 74 75 76
        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.',
77 78 79 80 81 82 83 84 85 86
      )..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',
87 88 89
        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.',
90 91 92 93
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
94
      )..addFlag('machine',
95 96 97 98
        hide: !verboseHelp,
        negatable: false,
        help: 'Handle machine structured JSON command input and provide output '
              'and progress in machine friendly format.',
99
      );
100
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
101
    hotRunnerFactory ??= HotRunnerFactory();
102 103
  }

104 105
  HotRunnerFactory hotRunnerFactory;

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

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

112
  int get debugPort {
113
    if (argResults['debug-port'] == null) {
114
      return null;
115
    }
116
    try {
117
      return int.parse(stringArg('debug-port'));
118 119 120 121 122 123
    } catch (error) {
      throwToolExit('Invalid port for `--debug-port`: $error');
    }
    return null;
  }

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

135
  String get appId {
136
    return stringArg('app-id');
137 138
  }

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

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

    await _validateArguments();

170
    writePidFile(stringArg('pid-file'));
171

172
    final Device device = await findTargetDevice();
173

174
    final Artifacts overrideArtifacts = device.artifactOverrides ?? globals.artifacts;
175 176 177
    await context.run<void>(
      body: () => _attachToDevice(device),
      overrides: <Type, Generator>{
178
        Artifacts: () => overrideArtifacts,
179 180 181 182 183 184 185
    });

    return null;
  }

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

199
    final Daemon daemon = boolArg('machine')
200 201 202 203 204 205 206
      ? Daemon(
          stdinCommandStream,
          stdoutCommandResponse,
          notifyingLogger: NotifyingLogger(),
          logToStdout: true,
          dartDefines: dartDefines,
        )
207 208
      : null;

209
    Stream<Uri> observatoryUri;
210 211 212 213 214
    bool usesIpv6 = ipv6;
    final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
    final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
    final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;

215
    if (devicePort == null && debugUri == null) {
216
      if (device is FuchsiaDevice) {
217
        final String module = stringArg('module');
218
        if (module == null) {
219
          throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
220
        }
221
        usesIpv6 = await device.ipv6;
222
        FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
223
        try {
224
          isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
225
          observatoryUri = Stream<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
226
        } catch (_) {
227
          isolateDiscoveryProtocol?.dispose();
228
          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
229
          for (final ForwardedPort port in ports) {
230 231
            await device.portForwarder.unforward(port);
          }
232
          rethrow;
233
        }
234
      } else if ((device is IOSDevice) || (device is IOSSimulator)) {
235 236 237 238 239 240 241 242 243 244
        final Uri uriFromMdns =
          await MDnsObservatoryDiscovery.instance.getObservatoryUri(
            appId,
            device,
            usesIpv6: usesIpv6,
            deviceVmservicePort: deviceVmservicePort,
          );
        observatoryUri = uriFromMdns == null
          ? null
          : Stream<Uri>.value(uriFromMdns).asBroadcastStream();
245 246 247
      }
      // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
      if (observatoryUri == null) {
248 249
        final ProtocolDiscovery observatoryDiscovery =
          ProtocolDiscovery.observatory(
250 251
            device.getLogReader(),
            portForwarder: device.portForwarder,
252 253 254
            ipv6: ipv6,
            devicePort: deviceVmservicePort,
            hostPort: hostVmservicePort,
255
          );
256
        globals.printStatus('Waiting for a connection from Flutter on ${device.name}...');
257 258 259
        observatoryUri = observatoryDiscovery.uris;
        // Determine ipv6 status from the scanned logs.
        usesIpv6 = observatoryDiscovery.ipv6;
260 261
      }
    } else {
262 263 264 265 266 267 268 269
      observatoryUri = Stream<Uri>
        .fromFuture(
          buildObservatoryUri(
            device,
            debugUri?.host ?? hostname,
            devicePort ?? debugUri.port,
            hostVmservicePort,
            debugUri?.path,
270
          )
271 272 273
        ).asBroadcastStream();
    }

274
    globals.terminal.usesTerminalUi = daemon == null;
275

276
    try {
277
      int result;
278
      if (daemon != null) {
279 280 281 282 283 284
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
285 286
        AppInstance app;
        try {
287 288 289 290 291 292
          app = await daemon.appDomain.launch(
            runner,
            runner.attach,
            device,
            null,
            true,
293
            globals.fs.currentDirectory,
294
            LaunchMode.attach,
295
          );
296 297 298
        } catch (error) {
          throwToolExit(error.toString());
        }
299 300
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
301 302 303 304 305 306 307 308 309
        return;
      }
      while (true) {
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
310
        final Completer<void> onAppStart = Completer<void>.sync();
311
        TerminalHandler terminalHandler;
312
        unawaited(onAppStart.future.whenComplete(() {
313
          terminalHandler = TerminalHandler(runner)
314 315 316 317 318 319
            ..setupTerminal()
            ..registerSignalHandlers();
        }));
        result = await runner.attach(
          appStartedCompleter: onAppStart,
        );
320 321 322 323
        if (result != 0) {
          throwToolExit(null, exitCode: result);
        }
        terminalHandler?.stop();
324
        assert(result != null);
325 326 327
        if (runner.exited || !runner.isWaitingForObservatory) {
          break;
        }
328
        globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
329
      }
330
    } finally {
331
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
332
      for (final ForwardedPort port in ports) {
333 334
        await device.portForwarder.unforward(port);
      }
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
  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,
      trackWidgetCreation: boolArg('track-widget-creation'),
      fileSystemRoots: stringsArg('filesystem-root'),
      fileSystemScheme: stringArg('filesystem-scheme'),
      viewFilter: stringArg('isolate-filter'),
      target: stringArg('target'),
      targetModel: TargetModel(stringArg('target-model')),
      buildMode: getBuildMode(),
      dartDefines: dartDefines,
    );
    flutterDevice.observatoryUris = observatoryUris;
    final List<FlutterDevice> flutterDevices =  <FlutterDevice>[flutterDevice];
    final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo());

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

384
  Future<void> _validateArguments() async { }
385
}
386 387

class HotRunnerFactory {
388 389 390 391 392 393 394 395 396 397 398 399
  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,
400
    FlutterProject flutterProject,
401
  }) => HotRunner(
402 403 404 405 406 407 408 409 410 411 412 413
    devices,
    target: target,
    debuggingOptions: debuggingOptions,
    benchmarkMode: benchmarkMode,
    applicationBinary: applicationBinary,
    hostIsIde: hostIsIde,
    projectRootPath: projectRootPath,
    packagesFilePath: packagesFilePath,
    dillOutputPath: dillOutputPath,
    stayResident: stayResident,
    ipv6: ipv6,
  );
414
}