attach.dart 12.4 KB
Newer Older
1 2 3 4 5 6
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import '../artifacts.dart';
8
import '../base/common.dart';
9
import '../base/context.dart';
10
import '../base/file_system.dart';
11
import '../base/io.dart';
12
import '../base/terminal.dart';
13
import '../base/utils.dart';
14
import '../cache.dart';
15
import '../commands/daemon.dart';
16
import '../compile.dart';
17
import '../device.dart';
18
import '../fuchsia/fuchsia_device.dart';
19
import '../globals.dart';
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
    usesIsolateFilterOption(hide: !verboseHelp);
59
    usesTargetOption();
60 61
    usesPortOptions();
    usesIpv6Flag();
62
    usesFilesystemOptions(hide: !verboseHelp);
63
    usesFuchsiaOptions(hide: !verboseHelp);
64
    usesDartDefines();
65 66
    argParser
      ..addOption(
67
        'debug-port',
68 69 70 71 72 73 74 75
        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.',
76 77 78 79 80 81 82 83 84 85
      )..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',
86 87 88
        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.',
89 90 91 92
      )..addOption(
        'project-root',
        hide: !verboseHelp,
        help: 'Normally used only in run target',
93
      )..addFlag('machine',
94 95 96 97
        hide: !verboseHelp,
        negatable: false,
        help: 'Handle machine structured JSON command input and provide output '
              'and progress in machine friendly format.',
98
      );
99
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
100
    hotRunnerFactory ??= HotRunnerFactory();
101 102
  }

103 104
  HotRunnerFactory hotRunnerFactory;

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

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

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

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

134 135 136 137
  String get appId {
    return argResults['app-id'];
  }

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

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

    await _validateArguments();

169 170
    writePidFile(argResults['pid-file']);

171
    final Device device = await findTargetDevice();
172 173 174 175 176 177 178 179 180 181 182 183 184

    final Artifacts artifacts = device.artifactOverrides ?? Artifacts.instance;
    await context.run<void>(
      body: () => _attachToDevice(device),
      overrides: <Type, Generator>{
        Artifacts: () => artifacts,
    });

    return null;
  }

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

    final Daemon daemon = argResults['machine']
199 200
      ? Daemon(stdinCommandStream, stdoutCommandResponse,
            notifyingLogger: NotifyingLogger(), logToStdout: true)
201 202
      : null;

203
    Uri observatoryUri;
204 205 206 207 208
    bool usesIpv6 = ipv6;
    final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
    final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
    final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;

209
    bool attachLogger = false;
210
    if (devicePort == null && debugUri == null) {
211 212 213
      if (device is FuchsiaDevice) {
        attachLogger = true;
        final String module = argResults['module'];
214
        if (module == null) {
215
          throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
216
        }
217 218
        usesIpv6 = device.ipv6;
        FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
219
        try {
220 221
          isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
          observatoryUri = await isolateDiscoveryProtocol.uri;
222
          printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
223
        } catch (_) {
224
          isolateDiscoveryProtocol?.dispose();
225 226 227 228
          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
          for (ForwardedPort port in ports) {
            await device.portForwarder.unforward(port);
          }
229
          rethrow;
230
        }
231
      } else if ((device is IOSDevice) || (device is IOSSimulator)) {
232 233 234
        observatoryUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri(
          appId,
          device,
235
          usesIpv6,
236
        );
237 238 239
      }
      // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
      if (observatoryUri == null) {
240 241 242 243 244 245 246 247
        ProtocolDiscovery observatoryDiscovery;
        try {
          observatoryDiscovery = ProtocolDiscovery.observatory(
            device.getLogReader(),
            portForwarder: device.portForwarder,
          );
          printStatus('Waiting for a connection from Flutter on ${device.name}...');
          observatoryUri = await observatoryDiscovery.uri;
248 249
          // Determine ipv6 status from the scanned logs.
          usesIpv6 = observatoryDiscovery.ipv6;
250
          printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
251 252
        } catch (error) {
          throwToolExit('Failed to establish a debug connection with ${device.name}: $error');
253 254 255
        } finally {
          await observatoryDiscovery?.cancel();
        }
256 257
      }
    } else {
258 259 260 261
      observatoryUri = await buildObservatoryUri(
        device,
        debugUri?.host ?? hostname,
        devicePort ?? debugUri.port,
262
        observatoryPort,
263 264
        debugUri?.path,
      );
265 266
    }
    try {
267
      final bool useHot = getBuildInfo().isDebug;
268
      final FlutterDevice flutterDevice = await FlutterDevice.create(
269
        device,
270
        flutterProject: flutterProject,
271
        trackWidgetCreation: argResults['track-widget-creation'],
272 273
        fileSystemRoots: argResults['filesystem-root'],
        fileSystemScheme: argResults['filesystem-scheme'],
274
        viewFilter: argResults['isolate-filter'],
275
        target: argResults['target'],
276
        targetModel: TargetModel(argResults['target-model']),
277
        buildMode: getBuildMode(),
278
        dartDefines: dartDefines,
279
      );
280
      flutterDevice.observatoryUris = <Uri>[ observatoryUri ];
281 282
      final List<FlutterDevice> flutterDevices =  <FlutterDevice>[flutterDevice];
      final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(getBuildInfo());
283
      terminal.usesTerminalUi = daemon == null;
284 285 286 287 288 289 290 291 292
      final ResidentRunner runner = useHot ?
          hotRunnerFactory.build(
            flutterDevices,
            target: targetFile,
            debuggingOptions: debuggingOptions,
            packagesFilePath: globalResults['packages'],
            projectRootPath: argResults['project-root'],
            dillOutputPath: argResults['output-dill'],
            ipv6: usesIpv6,
293
            flutterProject: flutterProject,
294 295 296 297 298 299 300
          )
        : ColdRunner(
            flutterDevices,
            target: targetFile,
            debuggingOptions: debuggingOptions,
            ipv6: usesIpv6,
          );
301 302 303
      if (attachLogger) {
        flutterDevice.startEchoingDeviceLog();
      }
304 305

      int result;
306 307 308
      if (daemon != null) {
        AppInstance app;
        try {
309 310 311 312 313 314 315
          app = await daemon.appDomain.launch(
            runner,
            runner.attach,
            device,
            null,
            true,
            fs.currentDirectory,
316
            LaunchMode.attach,
317
          );
318 319 320
        } catch (error) {
          throwToolExit(error.toString());
        }
321 322
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
323
      } else {
324 325 326 327 328 329 330 331 332
        final Completer<void> onAppStart = Completer<void>.sync();
        unawaited(onAppStart.future.whenComplete(() {
          TerminalHandler(runner)
            ..setupTerminal()
            ..registerSignalHandlers();
        }));
        result = await runner.attach(
          appStartedCompleter: onAppStart,
        );
333
        assert(result != null);
334
      }
335
      if (result != 0) {
336
        throwToolExit(null, exitCode: result);
337
      }
338
    } finally {
339
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
340 341 342
      for (ForwardedPort port in ports) {
        await device.portForwarder.unforward(port);
      }
343 344 345
    }
  }

346
  Future<void> _validateArguments() async { }
347
}
348 349

class HotRunnerFactory {
350 351 352 353 354 355 356 357 358 359 360 361
  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,
362
    FlutterProject flutterProject,
363
  }) => HotRunner(
364 365 366 367 368 369 370 371 372 373 374 375
    devices,
    target: target,
    debuggingOptions: debuggingOptions,
    benchmarkMode: benchmarkMode,
    applicationBinary: applicationBinary,
    hostIsIde: hostIsIde,
    projectRootPath: projectRootPath,
    packagesFilePath: packagesFilePath,
    dillOutputPath: dillOutputPath,
    stayResident: stayResident,
    ipv6: ipv6,
  );
376
}