attach.dart 13.5 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 15
import '../base/io.dart';
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
    usesDartDefineOption();
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
    } on Exception catch (error) {
119 120 121 122 123
      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
    return FlutterCommandResult.success();
182 183 184 185
  }

  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
      ? Daemon(
          stdinCommandStream,
          stdoutCommandResponse,
          notifyingLogger: NotifyingLogger(),
          logToStdout: true,
        )
206 207
      : null;

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

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

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

277
    try {
278
      int result;
279
      if (daemon != null) {
280 281 282 283 284 285
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
286 287
        AppInstance app;
        try {
288 289 290 291 292 293
          app = await daemon.appDomain.launch(
            runner,
            runner.attach,
            device,
            null,
            true,
294
            globals.fs.currentDirectory,
295
            LaunchMode.attach,
296
          );
297
        } on Exception catch (error) {
298 299
          throwToolExit(error.toString());
        }
300 301
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
302 303 304 305 306 307 308 309 310
        return;
      }
      while (true) {
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
311
        final Completer<void> onAppStart = Completer<void>.sync();
312
        TerminalHandler terminalHandler;
313
        unawaited(onAppStart.future.whenComplete(() {
314
          terminalHandler = TerminalHandler(runner)
315 316 317 318 319 320
            ..setupTerminal()
            ..registerSignalHandlers();
        }));
        result = await runner.attach(
          appStartedCompleter: onAppStart,
        );
321 322 323 324
        if (result != 0) {
          throwToolExit(null, exitCode: result);
        }
        terminalHandler?.stop();
325
        assert(result != null);
326 327 328
        if (runner.exited || !runner.isWaitingForObservatory) {
          break;
        }
329
        globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
330
      }
331
    } finally {
332
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
333
      for (final ForwardedPort port in ports) {
334 335
        await device.portForwarder.unforward(port);
      }
336 337 338
    }
  }

339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  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'),
      viewFilter: stringArg('isolate-filter'),
      target: stringArg('target'),
      targetModel: TargetModel(stringArg('target-model')),
358
      buildInfo: getBuildInfo(),
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
    );
    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,
        );
  }

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

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