device.dart 34.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
Devon Carew's avatar
Devon Carew committed
6
import 'dart:math' as math;
7

8
import 'package:meta/meta.dart';
9
import 'package:process/process.dart';
10
import 'package:vm_service/vm_service.dart' as vm_service;
11

12
import 'android/android_device_discovery.dart';
13
import 'android/android_sdk.dart';
14
import 'android/android_workflow.dart';
15
import 'application_package.dart';
16
import 'artifacts.dart';
17
import 'base/common.dart';
18
import 'base/config.dart';
19
import 'base/context.dart';
20
import 'base/dds.dart';
21
import 'base/file_system.dart';
22
import 'base/io.dart';
23
import 'base/logger.dart';
24
import 'base/os.dart';
25
import 'base/platform.dart';
26 27
import 'base/terminal.dart';
import 'base/user_messages.dart' hide userMessages;
28
import 'base/utils.dart';
29
import 'build_info.dart';
30
import 'devfs.dart';
31
import 'features.dart';
32
import 'fuchsia/fuchsia_device.dart';
33 34
import 'fuchsia/fuchsia_sdk.dart';
import 'fuchsia/fuchsia_workflow.dart';
35
import 'globals.dart' as globals show logger;
36
import 'ios/devices.dart';
37
import 'ios/ios_workflow.dart';
38
import 'ios/simulators.dart';
39 40
import 'linux/linux_device.dart';
import 'macos/macos_device.dart';
41
import 'macos/macos_workflow.dart';
42
import 'macos/xcode.dart';
43
import 'project.dart';
44
import 'tester/flutter_tester.dart';
45
import 'version.dart';
46
import 'web/web_device.dart';
47
import 'windows/windows_device.dart';
48
import 'windows/windows_workflow.dart';
49

50
DeviceManager get deviceManager => context.get<DeviceManager>();
51

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
/// A description of the kind of workflow the device supports.
class Category {
  const Category._(this.value);

  static const Category web = Category._('web');
  static const Category desktop = Category._('desktop');
  static const Category mobile = Category._('mobile');

  final String value;

  @override
  String toString() => value;
}

/// The platform sub-folder that a device type supports.
class PlatformType {
  const PlatformType._(this.value);

  static const PlatformType web = PlatformType._('web');
  static const PlatformType android = PlatformType._('android');
  static const PlatformType ios = PlatformType._('ios');
  static const PlatformType linux = PlatformType._('linux');
  static const PlatformType macos = PlatformType._('macos');
  static const PlatformType windows = PlatformType._('windows');
  static const PlatformType fuchsia = PlatformType._('fuchsia');

  final String value;

  @override
  String toString() => value;
}

84
/// A discovery mechanism for flutter-supported development devices.
85
abstract class DeviceManager {
86 87 88 89 90 91 92 93 94 95 96
  DeviceManager({
    @required Logger logger,
    @required Terminal terminal,
    @required UserMessages userMessages,
  }) : _logger = logger,
       _terminal = terminal,
       _userMessages = userMessages;

  final Logger _logger;
  final Terminal _terminal;
  final UserMessages _userMessages;
97

98
  /// Constructing DeviceManagers is cheap; they only do expensive work if some
99
  /// of their methods are called.
100
  List<DeviceDiscovery> get deviceDiscoverers;
101

102 103
  String _specifiedDeviceId;

104
  /// A user-specified device ID.
105
  String get specifiedDeviceId {
106
    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') {
107
      return null;
108
    }
109 110
    return _specifiedDeviceId;
  }
111

112 113 114 115 116
  set specifiedDeviceId(String id) {
    _specifiedDeviceId = id;
  }

  /// True when the user has specified a single specific device.
117
  bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
118

119 120 121 122
  /// True when the user has specified all devices by setting
  /// specifiedDeviceId = 'all'.
  bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';

123
  Future<List<Device>> getDevicesById(String deviceId) async {
124
    final String lowerDeviceId = deviceId.toLowerCase();
125
    bool exactlyMatchesDeviceId(Device device) =>
126 127
        device.id.toLowerCase() == lowerDeviceId ||
        device.name.toLowerCase() == lowerDeviceId;
128
    bool startsWithDeviceId(Device device) =>
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
        device.id.toLowerCase().startsWith(lowerDeviceId) ||
        device.name.toLowerCase().startsWith(lowerDeviceId);

    // Some discoverers have hard-coded device IDs and return quickly, and others
    // shell out to other processes and can take longer.
    // Process discoverers as they can return results, so if an exact match is
    // found quickly, we don't wait for all the discoverers to complete.
    final List<Device> prefixMatches = <Device>[];
    final Completer<Device> exactMatchCompleter = Completer<Device>();
    final List<Future<List<Device>>> futureDevices = <Future<List<Device>>>[
      for (final DeviceDiscovery discoverer in _platformDiscoverers)
        discoverer
        .devices
        .then((List<Device> devices) {
          for (final Device device in devices) {
            if (exactlyMatchesDeviceId(device)) {
              exactMatchCompleter.complete(device);
              return null;
            }
            if (startsWithDeviceId(device)) {
              prefixMatches.add(device);
            }
          }
          return null;
        }, onError: (dynamic error, StackTrace stackTrace) {
          // Return matches from other discoverers even if one fails.
155
          _logger.printTrace('Ignored error discovering $deviceId: $error');
156 157 158 159 160 161 162 163 164 165 166
        })
    ];

    // Wait for an exact match, or for all discoverers to return results.
    await Future.any<dynamic>(<Future<dynamic>>[
      exactMatchCompleter.future,
      Future.wait<List<Device>>(futureDevices),
    ]);

    if (exactMatchCompleter.isCompleted) {
      return <Device>[await exactMatchCompleter.future];
167
    }
168
    return prefixMatches;
Devon Carew's avatar
Devon Carew committed
169 170
  }

171
  /// Returns the list of connected devices, filtered by any user-specified device id.
172
  Future<List<Device>> getDevices() {
173 174 175
    return hasSpecifiedDeviceId
        ? getDevicesById(specifiedDeviceId)
        : getAllConnectedDevices();
176 177
  }

178
  Iterable<DeviceDiscovery> get _platformDiscoverers {
179
    return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform);
180 181
  }

182
  /// Returns the list of all connected devices.
183 184 185 186 187 188 189
  Future<List<Device>> getAllConnectedDevices() async {
    final List<List<Device>> devices = await Future.wait<List<Device>>(<Future<List<Device>>>[
      for (final DeviceDiscovery discoverer in _platformDiscoverers)
        discoverer.devices,
    ]);

    return devices.expand<Device>((List<Device> deviceList) => deviceList).toList();
190
  }
191

192 193 194 195 196 197 198 199 200 201
  /// Returns the list of all connected devices. Discards existing cache of devices.
  Future<List<Device>> refreshAllConnectedDevices({ Duration timeout }) async {
    final List<List<Device>> devices = await Future.wait<List<Device>>(<Future<List<Device>>>[
      for (final DeviceDiscovery discoverer in _platformDiscoverers)
        discoverer.discoverDevices(timeout: timeout),
    ]);

    return devices.expand<Device>((List<Device> deviceList) => deviceList).toList();
  }

202 203 204 205 206 207 208
  /// Whether we're capable of listing any devices given the current environment configuration.
  bool get canListAnything {
    return _platformDiscoverers.any((DeviceDiscovery discoverer) => discoverer.canListAnything);
  }

  /// Get diagnostics about issues with any connected devices.
  Future<List<String>> getDeviceDiagnostics() async {
209
    return <String>[
210
      for (final DeviceDiscovery discoverer in _platformDiscoverers)
211 212
        ...await discoverer.getDiagnostics(),
    ];
213
  }
214 215 216

  /// Find and return a list of devices based on the current project and environment.
  ///
217
  /// Returns a list of devices specified by the user.
218 219 220 221 222 223 224 225 226 227
  ///
  /// * If the user specified '-d all', then return all connected devices which
  /// support the current project, except for fuchsia and web.
  ///
  /// * If the user specified a device id, then do nothing as the list is already
  /// filtered by [getDevices].
  ///
  /// * If the user did not specify a device id and there is more than one
  /// device connected, then filter out unsupported devices and prioritize
  /// ephemeral devices.
228 229 230
  ///
  /// * If [flutterProject] is null, then assume the project supports all
  /// device types.
231 232 233 234 235 236
  Future<List<Device>> findTargetDevices(FlutterProject flutterProject, { Duration timeout }) async {
    if (timeout != null) {
      // Reset the cache with the specified timeout.
      await refreshAllConnectedDevices(timeout: timeout);
    }

237
    List<Device> devices = await getDevices();
238

239 240
    // Always remove web and fuchsia devices from `--all`. This setting
    // currently requires devices to share a frontend_server and resident
241
    // runner instance. Both web and fuchsia require differently configured
242 243 244
    // compilers, and web requires an entirely different resident runner.
    if (hasSpecifiedAllDevices) {
      devices = <Device>[
245
        for (final Device device in devices)
246 247
          if (await device.targetPlatform != TargetPlatform.fuchsia_arm64 &&
              await device.targetPlatform != TargetPlatform.fuchsia_x64 &&
248
              await device.targetPlatform != TargetPlatform.web_javascript)
249
            device,
250 251 252 253 254 255 256 257
      ];
    }

    // If there is no specified device, the remove all devices which are not
    // supported by the current application. For example, if there was no
    // 'android' folder then don't attempt to launch with an Android device.
    if (devices.length > 1 && !hasSpecifiedDeviceId) {
      devices = <Device>[
258
        for (final Device device in devices)
259
          if (isDeviceSupportedForProject(device, flutterProject))
260
            device,
261
      ];
262 263 264 265 266 267
    } else if (devices.length == 1 &&
             !hasSpecifiedDeviceId &&
             !isDeviceSupportedForProject(devices.single, flutterProject)) {
      // If there is only a single device but it is not supported, then return
      // early.
      return <Device>[];
268
    }
269

270 271
    // If there are still multiple devices and the user did not specify to run
    // all, then attempt to prioritize ephemeral devices. For example, if the
272
    // user only typed 'flutter run' and both an Android device and desktop
273
    // device are available, choose the Android device.
274
    if (devices.length > 1 && !hasSpecifiedAllDevices) {
275 276 277
      // Note: ephemeral is nullable for device types where this is not well
      // defined.
      if (devices.any((Device device) => device.ephemeral == true)) {
278 279
        // if there is only one ephemeral device, get it
        final List<Device> ephemeralDevices = devices
280 281
            .where((Device device) => device.ephemeral == true)
            .toList();
282 283 284 285 286 287 288 289 290

            if (ephemeralDevices.length == 1){
              devices = ephemeralDevices;
            }
      }
      // If it was not able to prioritize a device. For example, if the user
      // has two active Android devices running, then we request the user to
      // choose one. If the user has two nonEphemeral devices running, we also
      // request input to choose one.
291 292 293
      if (devices.length > 1 && _terminal.stdinHasTerminal) {
        _logger.printStatus(_userMessages.flutterMultipleDevicesFound);
        await Device.printDevices(devices, _logger);
294
        final Device chosenDevice = await _chooseOneOfAvailableDevices(devices);
295
        specifiedDeviceId = chosenDevice.id;
296
        devices = <Device>[chosenDevice];
297 298 299 300 301
      }
    }
    return devices;
  }

302 303 304
  Future<Device> _chooseOneOfAvailableDevices(List<Device> devices) async {
    _displayDeviceOptions(devices);
    final String userInput =  await _readUserInput(devices.length);
305 306 307
    if (userInput.toLowerCase() == 'q') {
      throwToolExit('');
    }
308 309 310 311 312 313
    return devices[int.parse(userInput)];
  }

  void _displayDeviceOptions(List<Device> devices) {
    int count = 0;
    for (final Device device in devices) {
314
      _logger.printStatus(_userMessages.flutterChooseDevice(count, device.name, device.id));
315 316 317 318 319
      count++;
    }
  }

  Future<String> _readUserInput(int deviceCount) async {
320 321 322 323 324 325 326
    _terminal.usesTerminalUi = true;
    final String result = await _terminal.promptForCharInput(
      <String>[ for (int i = 0; i < deviceCount; i++) '$i', 'q', 'Q'],
      displayAcceptedCharacters: false,
      logger: _logger,
      prompt: _userMessages.flutterChooseOne,
    );
327 328 329
    return result;
  }

330
  /// Returns whether the device is supported for the project.
331
  ///
332 333
  /// This exists to allow the check to be overridden for google3 clients. If
  /// [flutterProject] is null then return true.
334
  bool isDeviceSupportedForProject(Device device, FlutterProject flutterProject) {
335 336 337
    if (flutterProject == null) {
      return true;
    }
338 339
    return device.isSupportedForProject(flutterProject);
  }
340 341
}

342
class FlutterDeviceManager extends DeviceManager {
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  FlutterDeviceManager({
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
    @required FileSystem fileSystem,
    @required AndroidSdk androidSdk,
    @required FeatureFlags featureFlags,
    @required IOSSimulatorUtils iosSimulatorUtils,
    @required XCDevice xcDevice,
    @required AndroidWorkflow androidWorkflow,
    @required IOSWorkflow iosWorkflow,
    @required FuchsiaWorkflow fuchsiaWorkflow,
    @required FlutterVersion flutterVersion,
    @required Config config,
    @required Artifacts artifacts,
358
    @required MacOSWorkflow macOSWorkflow,
359
    @required UserMessages userMessages,
360 361
    @required OperatingSystemUtils operatingSystemUtils,
    @required WindowsWorkflow windowsWorkflow,
362
    @required Terminal terminal,
363
  }) : deviceDiscoverers =  <DeviceDiscovery>[
364
    AndroidDevices(
365 366
      logger: logger,
      androidSdk: androidSdk,
367
      androidWorkflow: androidWorkflow,
368
      processManager: processManager,
369 370 371
      fileSystem: fileSystem,
      platform: platform,
      userMessages: userMessages,
372 373
    ),
    IOSDevices(
374 375 376 377 378 379 380
      platform: platform,
      xcdevice: xcDevice,
      iosWorkflow: iosWorkflow,
      logger: logger,
    ),
    IOSSimulators(
      iosSimulatorUtils: iosSimulatorUtils,
381 382 383
    ),
    FuchsiaDevices(
      fuchsiaSdk: fuchsiaSdk,
384
      logger: logger,
385
      fuchsiaWorkflow: fuchsiaWorkflow,
386 387 388 389 390 391 392 393 394
      platform: platform,
    ),
    FlutterTesterDevices(
      fileSystem: fileSystem,
      flutterVersion: flutterVersion,
      processManager: processManager,
      config: config,
      logger: logger,
      artifacts: artifacts,
395
    ),
396 397 398 399 400
    MacOSDevices(
      processManager: processManager,
      macOSWorkflow: macOSWorkflow,
      logger: logger,
      platform: platform,
401
      fileSystem: fileSystem,
402
      operatingSystemUtils: operatingSystemUtils,
403
    ),
404
    LinuxDevices(
405
      platform: platform,
406
      featureFlags: featureFlags,
407 408
      processManager: processManager,
      logger: logger,
409
      fileSystem: fileSystem,
410 411 412 413 414 415 416 417
      operatingSystemUtils: operatingSystemUtils,
    ),
    WindowsDevices(
      processManager: processManager,
      operatingSystemUtils: operatingSystemUtils,
      logger: logger,
      fileSystem: fileSystem,
      windowsWorkflow: windowsWorkflow,
418 419 420
    ),
    WebDevices(
      featureFlags: featureFlags,
421 422 423 424
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      logger: logger,
425
    ),
426 427 428 429 430
  ], super(
      logger: logger,
      terminal: terminal,
      userMessages: userMessages,
    );
431 432 433

  @override
  final List<DeviceDiscovery> deviceDiscoverers;
434 435
}

436 437 438
/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
  bool get supportsPlatform;
439 440 441 442 443

  /// Whether this device discovery is capable of listing any devices given the
  /// current environment configuration.
  bool get canListAnything;

444
  /// Return all connected devices, cached on subsequent calls.
445
  Future<List<Device>> get devices;
446

447 448 449
  /// Return all connected devices. Discards existing cache of devices.
  Future<List<Device>> discoverDevices({ Duration timeout });

450 451
  /// Gets a list of diagnostic messages pertaining to issues with any connected
  /// devices (will be an empty list if there are no issues).
452
  Future<List<String>> getDiagnostics() => Future<List<String>>.value(<String>[]);
453 454
}

455 456 457 458 459
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
/// and removals.
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
  PollingDeviceDiscovery(this.name);

460 461
  static const Duration _pollingInterval = Duration(seconds: 4);
  static const Duration _pollingTimeout = Duration(seconds: 30);
462 463

  final String name;
464 465 466 467 468

  @protected
  @visibleForTesting
  ItemListNotifier<Device> deviceNotifier;

469
  Timer _timer;
470

471
  Future<List<Device>> pollingGetDevices({ Duration timeout });
472

473
  void startPolling() {
474
    if (_timer == null) {
475
      deviceNotifier ??= ItemListNotifier<Device>();
476 477
      // Make initial population the default, fast polling timeout.
      _timer = _initTimer(null);
478 479 480
    }
  }

481
  Timer _initTimer(Duration pollingTimeout) {
482 483
    return Timer(_pollingInterval, () async {
      try {
484
        final List<Device> devices = await pollingGetDevices(timeout: pollingTimeout);
485
        deviceNotifier.updateWithNewList(devices);
486
      } on TimeoutException {
487
        // Do nothing on a timeout.
488
      }
489 490
      // Subsequent timeouts after initial population should wait longer.
      _timer = _initTimer(_pollingTimeout);
491 492 493
    });
  }

494
  void stopPolling() {
495 496
    _timer?.cancel();
    _timer = null;
497 498
  }

499
  @override
500
  Future<List<Device>> get devices {
501 502 503 504
    return _populateDevices();
  }

  @override
505
  Future<List<Device>> discoverDevices({ Duration timeout }) {
506
    deviceNotifier = null;
507 508 509 510
    return _populateDevices(timeout: timeout);
  }

  Future<List<Device>> _populateDevices({ Duration timeout }) async {
511 512
    deviceNotifier ??= ItemListNotifier<Device>.from(await pollingGetDevices(timeout: timeout));
    return deviceNotifier.items;
513 514 515
  }

  Stream<Device> get onAdded {
516 517
    deviceNotifier ??= ItemListNotifier<Device>();
    return deviceNotifier.onAdded;
518 519 520
  }

  Stream<Device> get onRemoved {
521 522
    deviceNotifier ??= ItemListNotifier<Device>();
    return deviceNotifier.onRemoved;
523 524
  }

525
  void dispose() => stopPolling();
526

527
  @override
528 529 530
  String toString() => '$name device discovery';
}

531 532 533 534
/// A device is a physical hardware that can run a flutter application.
///
/// This may correspond to a connected iOS or Android device, or represent
/// the host operating system in the case of Flutter Desktop.
535
abstract class Device {
536 537 538 539 540
  Device(this.id, {
    @required this.category,
    @required this.platformType,
    @required this.ephemeral,
  });
541

542
  final String id;
543

544 545 546 547 548 549 550 551 552
  /// The [Category] for this device type.
  final Category category;

  /// The [PlatformType] for this device.
  final PlatformType platformType;

  /// Whether this is an ephemeral device.
  final bool ephemeral;

553 554
  String get name;

555 556
  bool get supportsStartPaused => true;

557
  /// Whether it is an emulated device running on localhost.
558 559 560
  ///
  /// This may return `true` for certain physical Android devices, and is
  /// generally only a best effort guess.
561
  Future<bool> get isLocalEmulator;
562

563 564 565 566 567 568 569 570
  /// The unique identifier for the emulator that corresponds to this device, or
  /// null if it is not an emulator.
  ///
  /// The ID returned matches that in the output of `flutter emulators`. Fetching
  /// this name may require connecting to the device and if an error occurs null
  /// will be returned.
  Future<String> get emulatorId;

571 572 573 574 575 576
  /// Whether this device can run the provided [buildMode].
  ///
  /// For example, some emulator architectures cannot run profile or
  /// release builds.
  FutureOr<bool> supportsRuntimeMode(BuildMode buildMode) => true;

577
  /// Whether the device is a simulator on a platform which supports hardware rendering.
578
  // This is soft-deprecated since the logic is not correct expect for iOS simulators.
579
  Future<bool> get supportsHardwareRendering async {
580
    return true;
581 582
  }

583 584 585
  /// Whether the device is supported for the current project directory.
  bool isSupportedForProject(FlutterProject flutterProject);

586 587 588 589 590 591 592
  /// Check if a version of the given app is already installed.
  ///
  /// Specify [userIdentifier] to check if installed for a particular user (Android only).
  Future<bool> isAppInstalled(
    covariant ApplicationPackage app, {
    String userIdentifier,
  });
593

594
  /// Check if the latest build of the [app] is already installed.
595
  Future<bool> isLatestBuildInstalled(covariant ApplicationPackage app);
596

597 598 599 600 601 602 603
  /// Install an app package on the current device.
  ///
  /// Specify [userIdentifier] to install for a particular user (Android only).
  Future<bool> installApp(
    covariant ApplicationPackage app, {
    String userIdentifier,
  });
604

605 606 607 608 609 610 611 612
  /// Uninstall an app package from the current device.
  ///
  /// Specify [userIdentifier] to uninstall for a particular user,
  /// defaults to all users (Android only).
  Future<bool> uninstallApp(
    covariant ApplicationPackage app, {
    String userIdentifier,
  });
613

614
  /// Check if the device is supported by Flutter.
615 616 617 618
  bool isSupported();

  // String meant to be displayed to the user indicating if the device is
  // supported by Flutter, and, if not, why.
619
  String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
620

621 622
  /// The device's platform.
  Future<TargetPlatform> get targetPlatform;
623

624 625 626 627
  /// Platform name for display only.
  Future<String> get targetPlatformDisplayName async =>
      getNameForTargetPlatform(await targetPlatform);

628
  Future<String> get sdkNameAndVersion;
629

630 631 632
  /// Create a platform-specific [DevFSWriter] for the given [app], or
  /// null if the device does not support them.
  ///
633
  /// For example, the desktop device classes can use a writer which
634 635 636 637 638 639 640 641
  /// copies the files across the local file system.
  DevFSWriter createDevFSWriter(
    covariant ApplicationPackage app,
    String userIdentifier,
  ) {
    return null;
  }

642
  /// Get a log reader for this device.
643 644
  ///
  /// If `app` is specified, this will return a log reader specific to that
645
  /// application. Otherwise, a global log reader will be returned.
646 647 648 649 650 651 652 653
  ///
  /// If `includePastLogs` is true and the device type supports it, the log
  /// reader will also include log messages from before the invocation time.
  /// Defaults to false.
  FutureOr<DeviceLogReader> getLogReader({
    covariant ApplicationPackage app,
    bool includePastLogs = false,
  });
654

655 656 657
  /// Get the port forwarder for this device.
  DevicePortForwarder get portForwarder;

658 659 660 661 662 663
  /// Get the DDS instance for this device.
  DartDevelopmentService get dds => _dds ??= DartDevelopmentService(
    logger: globals.logger,
  );
  DartDevelopmentService _dds;

664 665
  /// Clear the device's logs.
  void clearLogs();
666

667 668 669
  /// Optional device-specific artifact overrides.
  OverrideArtifacts get artifactOverrides => null;

670 671 672
  /// Start an app package on the current device.
  ///
  /// [platformArgs] allows callers to pass platform-specific arguments to the
673
  /// start call. The build mode is not used by all platforms.
Devon Carew's avatar
Devon Carew committed
674
  Future<LaunchResult> startApp(
675
    covariant ApplicationPackage package, {
676 677
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
678
    DebuggingOptions debuggingOptions,
679
    Map<String, dynamic> platformArgs,
680 681
    bool prebuiltApplication = false,
    bool ipv6 = false,
682
    String userIdentifier,
683
  });
684

685 686 687 688 689
  /// Whether this device implements support for hot reload.
  bool get supportsHotReload => true;

  /// Whether this device implements support for hot restart.
  bool get supportsHotRestart => true;
690

691
  /// Whether flutter applications running on this device can be terminated
692
  /// from the VM Service.
693
  bool get supportsFlutterExit => true;
694

695 696
  /// Whether the device supports taking screenshots of a running flutter
  /// application.
Devon Carew's avatar
Devon Carew committed
697 698
  bool get supportsScreenshot => false;

699 700 701
  /// Whether the device supports the '--fast-start' development mode.
  bool get supportsFastStart => false;

702
  /// Stop an app package on the current device.
703 704 705 706 707 708
  ///
  /// Specify [userIdentifier] to stop app installed to a profile (Android only).
  Future<bool> stopApp(
    covariant ApplicationPackage app, {
    String userIdentifier,
  });
709

710 711 712 713 714 715 716 717
  /// Query the current application memory usage..
  ///
  /// If the device does not support this callback, an empty map
  /// is returned.
  Future<MemoryInfo> queryMemoryInfo() {
    return Future<MemoryInfo>.value(const MemoryInfo.empty());
  }

718
  Future<void> takeScreenshot(File outputFile) => Future<void>.error('unimplemented');
Devon Carew's avatar
Devon Carew committed
719

720
  @nonVirtual
721
  @override
722
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
723 724
  int get hashCode => id.hashCode;

725
  @nonVirtual
726
  @override
727
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
728
  bool operator ==(Object other) {
729
    if (identical(this, other)) {
730
      return true;
731
    }
732 733
    return other is Device
        && other.id == id;
734 735
  }

736
  @override
737
  String toString() => name;
Devon Carew's avatar
Devon Carew committed
738

739
  static Stream<String> descriptions(List<Device> devices) async* {
740
    if (devices.isEmpty) {
741
      return;
742
    }
Devon Carew's avatar
Devon Carew committed
743

744
    // Extract device information
745
    final List<List<String>> table = <List<String>>[];
746
    for (final Device device in devices) {
Devon Carew's avatar
Devon Carew committed
747
      String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
748 749 750
      final TargetPlatform targetPlatform = await device.targetPlatform;
      if (await device.isLocalEmulator) {
        final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
751 752
        supportIndicator += ' ($type)';
      }
753
      table.add(<String>[
754
        '${device.name} (${device.category})',
755
        device.id,
756
        await device.targetPlatformDisplayName,
757
        '${await device.sdkNameAndVersion}$supportIndicator',
758 759 760 761
      ]);
    }

    // Calculate column widths
762
    final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i);
763
    List<int> widths = indices.map<int>((int i) => 0).toList();
764
    for (final List<String> row in table) {
765
      widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
766 767 768
    }

    // Join columns into lines of text
769
    for (final List<String> row in table) {
770
      yield indices.map<String>((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
771
    }
772 773
  }

774 775
  static Future<void> printDevices(List<Device> devices, Logger logger) async {
    await descriptions(devices).forEach(logger.printStatus);
Devon Carew's avatar
Devon Carew committed
776
  }
777

778 779 780 781 782 783 784
  static List<String> devicesPlatformTypes(List<Device> devices) {
    return devices
        .map(
          (Device d) => d.platformType.toString(),
        ).toSet().toList()..sort();
  }

785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
  /// Convert the Device object to a JSON representation suitable for serialization.
  Future<Map<String, Object>> toJson() async {
    final bool isLocalEmu = await isLocalEmulator;
    return <String, Object>{
      'name': name,
      'id': id,
      'isSupported': isSupported(),
      'targetPlatform': getNameForTargetPlatform(await targetPlatform),
      'emulator': isLocalEmu,
      'sdk': await sdkNameAndVersion,
      'capabilities': <String, Object>{
        'hotReload': supportsHotReload,
        'hotRestart': supportsHotRestart,
        'screenshot': supportsScreenshot,
        'fastStart': supportsFastStart,
        'flutterExit': supportsFlutterExit,
        'hardwareRendering': isLocalEmu && await supportsHardwareRendering,
        'startPaused': supportsStartPaused,
      }
    };
  }

807
  /// Clean up resources allocated by device.
808 809
  ///
  /// For example log readers or port forwarders.
810
  Future<void> dispose();
811 812
}

813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831
/// Information about an application's memory usage.
abstract class MemoryInfo {
  /// Const constructor to allow subclasses to be const.
  const MemoryInfo();

  /// Create a [MemoryInfo] object with no information.
  const factory MemoryInfo.empty() = _NoMemoryInfo;

  /// Convert the object to a JSON representation suitable for serialization.
  Map<String, Object> toJson();
}

class _NoMemoryInfo implements MemoryInfo {
  const _NoMemoryInfo();

  @override
  Map<String, Object> toJson() => <String, Object>{};
}

Devon Carew's avatar
Devon Carew committed
832
class DebuggingOptions {
833 834
  DebuggingOptions.enabled(
    this.buildInfo, {
835
    this.startPaused = false,
836
    this.disableServiceAuthCodes = false,
837
    this.disableDds = false,
838
    this.dartEntrypointArgs = const <String>[],
839
    this.dartFlags = '',
840 841 842
    this.enableSoftwareRendering = false,
    this.skiaDeterministicRendering = false,
    this.traceSkia = false,
843
    this.traceAllowlist,
844
    this.traceSystrace = false,
845
    this.endlessTraceBuffer = false,
846
    this.dumpSkpOnShaderCompilation = false,
847
    this.cacheSkSL = false,
848
    this.purgePersistentCache = false,
849
    this.useTestFonts = false,
850
    this.verboseSystemLogs = false,
851
    this.hostVmServicePort,
852
    this.disablePortPublication = false,
853
    this.deviceVmServicePort,
854
    this.ddsPort,
855 856
    this.hostname,
    this.port,
857
    this.webEnableExposeUrl,
858
    this.webUseSseForDebugProxy = true,
859
    this.webUseSseForDebugBackend = true,
860 861
    this.webRunHeadless = false,
    this.webBrowserDebugPort,
862
    this.webEnableExpressionEvaluation = false,
863
    this.vmserviceOutFile,
864
    this.fastStart = false,
865
    this.nullAssertions = false,
Devon Carew's avatar
Devon Carew committed
866 867
   }) : debuggingEnabled = true;

868
  DebuggingOptions.disabled(this.buildInfo, {
869
      this.dartEntrypointArgs = const <String>[],
870 871 872
      this.port,
      this.hostname,
      this.webEnableExposeUrl,
873
      this.webUseSseForDebugProxy = true,
874
      this.webUseSseForDebugBackend = true,
875 876
      this.webRunHeadless = false,
      this.webBrowserDebugPort,
877
      this.cacheSkSL = false,
878
      this.traceAllowlist,
879
    }) : debuggingEnabled = false,
880 881
      useTestFonts = false,
      startPaused = false,
882
      dartFlags = '',
883
      disableServiceAuthCodes = false,
884
      disableDds = false,
885 886 887 888
      enableSoftwareRendering = false,
      skiaDeterministicRendering = false,
      traceSkia = false,
      traceSystrace = false,
889
      endlessTraceBuffer = false,
890
      dumpSkpOnShaderCompilation = false,
891
      purgePersistentCache = false,
892
      verboseSystemLogs = false,
893
      hostVmServicePort = null,
894
      disablePortPublication = false,
895
      deviceVmServicePort = null,
896
      ddsPort = null,
897
      vmserviceOutFile = null,
898
      fastStart = false,
899 900
      webEnableExpressionEvaluation = false,
      nullAssertions = false;
Devon Carew's avatar
Devon Carew committed
901 902 903

  final bool debuggingEnabled;

904
  final BuildInfo buildInfo;
Devon Carew's avatar
Devon Carew committed
905
  final bool startPaused;
906
  final String dartFlags;
907
  final List<String> dartEntrypointArgs;
908
  final bool disableServiceAuthCodes;
909
  final bool disableDds;
910
  final bool enableSoftwareRendering;
911
  final bool skiaDeterministicRendering;
912
  final bool traceSkia;
913
  final String traceAllowlist;
914
  final bool traceSystrace;
915
  final bool endlessTraceBuffer;
916
  final bool dumpSkpOnShaderCompilation;
917
  final bool cacheSkSL;
918
  final bool purgePersistentCache;
919
  final bool useTestFonts;
920
  final bool verboseSystemLogs;
921 922
  final int hostVmServicePort;
  final int deviceVmServicePort;
923
  final bool disablePortPublication;
924
  final int ddsPort;
925 926
  final String port;
  final String hostname;
927
  final bool webEnableExposeUrl;
928
  final bool webUseSseForDebugProxy;
929
  final bool webUseSseForDebugBackend;
930 931 932 933 934 935 936 937 938 939 940

  /// Whether to run the browser in headless mode.
  ///
  /// Some CI environments do not provide a display and fail to launch the
  /// browser with full graphics stack. Some browsers provide a special
  /// "headless" mode that runs the browser with no graphics.
  final bool webRunHeadless;

  /// The port the browser should use for its debugging protocol.
  final int webBrowserDebugPort;

941
  /// Enable expression evaluation for web target.
942 943
  final bool webEnableExpressionEvaluation;

944
  /// A file where the VM Service URL should be written after the application is started.
945
  final String vmserviceOutFile;
946
  final bool fastStart;
Devon Carew's avatar
Devon Carew committed
947

948 949
  final bool nullAssertions;

950
  bool get hasObservatoryPort => hostVmServicePort != null;
Devon Carew's avatar
Devon Carew committed
951 952 953
}

class LaunchResult {
954
  LaunchResult.succeeded({ this.observatoryUri }) : started = true;
955 956 957
  LaunchResult.failed()
    : started = false,
      observatoryUri = null;
Devon Carew's avatar
Devon Carew committed
958

959
  bool get hasObservatory => observatoryUri != null;
Devon Carew's avatar
Devon Carew committed
960 961

  final bool started;
962
  final Uri observatoryUri;
Devon Carew's avatar
Devon Carew committed
963 964 965

  @override
  String toString() {
966
    final StringBuffer buf = StringBuffer('started=$started');
967
    if (observatoryUri != null) {
968
      buf.write(', observatory=$observatoryUri');
969
    }
Devon Carew's avatar
Devon Carew committed
970 971 972 973
    return buf.toString();
  }
}

974
class ForwardedPort {
975 976
  ForwardedPort(this.hostPort, this.devicePort) : context = null;
  ForwardedPort.withContext(this.hostPort, this.devicePort, this.context);
977 978 979

  final int hostPort;
  final int devicePort;
980
  final Process context;
981

982
  @override
983
  String toString() => 'ForwardedPort HOST:$hostPort to DEVICE:$devicePort';
984 985 986

  /// Kill subprocess (if present) used in forwarding.
  void dispose() {
987 988
    if (context != null) {
      context.kill();
989 990
    }
  }
991 992 993 994 995 996 997 998 999
}

/// Forward ports from the host machine to the device.
abstract class DevicePortForwarder {
  /// Returns a Future that completes with the current list of forwarded
  /// ports for this device.
  List<ForwardedPort> get forwardedPorts;

  /// Forward [hostPort] on the host to [devicePort] on the device.
1000
  /// If [hostPort] is null or zero, will auto select a host port.
1001
  /// Returns a Future that completes with the host port.
1002
  Future<int> forward(int devicePort, { int hostPort });
1003 1004

  /// Stops forwarding [forwardedPort].
1005
  Future<void> unforward(ForwardedPort forwardedPort);
1006

1007
  /// Cleanup allocated resources, like [forwardedPorts].
1008
  Future<void> dispose();
1009 1010
}

Devon Carew's avatar
Devon Carew committed
1011
/// Read the log for a particular device.
Devon Carew's avatar
Devon Carew committed
1012 1013 1014
abstract class DeviceLogReader {
  String get name;

Devon Carew's avatar
Devon Carew committed
1015 1016
  /// A broadcast stream where each element in the string is a line of log output.
  Stream<String> get logLines;
Devon Carew's avatar
Devon Carew committed
1017

1018 1019
  /// Some logs can be obtained from a VM service stream.
  /// Set this after the VM services are connected.
1020
  vm_service.VmService connectedVMService;
1021

1022
  @override
Devon Carew's avatar
Devon Carew committed
1023
  String toString() => name;
1024

1025
  /// Process ID of the app on the device.
1026
  int appPid;
1027 1028

  // Clean up resources allocated by log reader e.g. subprocesses
1029
  void dispose();
Devon Carew's avatar
Devon Carew committed
1030
}
1031 1032 1033

/// Describes an app running on the device.
class DiscoveredApp {
1034
  DiscoveredApp(this.id, this.observatoryPort);
1035 1036 1037
  final String id;
  final int observatoryPort;
}
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048

// An empty device log reader
class NoOpDeviceLogReader implements DeviceLogReader {
  NoOpDeviceLogReader(this.name);

  @override
  final String name;

  @override
  int appPid;

1049
  @override
1050
  vm_service.VmService connectedVMService;
1051

1052 1053
  @override
  Stream<String> get logLines => const Stream<String>.empty();
1054 1055 1056

  @override
  void dispose() { }
1057 1058
}

1059
// A port forwarder which does not support forwarding ports.
1060 1061 1062 1063
class NoOpDevicePortForwarder implements DevicePortForwarder {
  const NoOpDevicePortForwarder();

  @override
1064
  Future<int> forward(int devicePort, { int hostPort }) async => devicePort;
1065 1066 1067 1068 1069

  @override
  List<ForwardedPort> get forwardedPorts => <ForwardedPort>[];

  @override
1070
  Future<void> unforward(ForwardedPort forwardedPort) async { }
1071 1072 1073

  @override
  Future<void> dispose() async { }
1074
}
1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085

/// Append --null_assertions to any existing Dart VM flags if
/// [debuggingOptions.nullAssertions] is true.
String computeDartVmFlags(DebuggingOptions debuggingOptions) {
  return <String>[
    if (debuggingOptions.dartFlags?.isNotEmpty ?? false)
      debuggingOptions.dartFlags,
    if (debuggingOptions.nullAssertions)
      '--null_assertions',
  ].join(',');
}