device.dart 30.8 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 9
import 'package:meta/meta.dart';

10
import 'application_package.dart';
11
import 'artifacts.dart';
12
import 'base/common.dart';
13
import 'base/context.dart';
14
import 'base/dds.dart';
15
import 'base/file_system.dart';
16
import 'base/logger.dart';
17 18
import 'base/terminal.dart';
import 'base/user_messages.dart' hide userMessages;
19
import 'base/utils.dart';
20
import 'build_info.dart';
21
import 'devfs.dart';
22
import 'device_port_forwarder.dart';
23
import 'project.dart';
24
import 'vmservice.dart';
25

26
DeviceManager? get deviceManager => context.get<DeviceManager>();
27

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
/// 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');
53
  static const PlatformType custom = PlatformType._('custom');
54 55 56 57 58 59 60

  final String value;

  @override
  String toString() => value;
}

61
/// A discovery mechanism for flutter-supported development devices.
62
abstract class DeviceManager {
63
  DeviceManager({
64 65 66
    required Logger logger,
    required Terminal terminal,
    required UserMessages userMessages,
67 68 69 70 71 72 73
  }) : _logger = logger,
       _terminal = terminal,
       _userMessages = userMessages;

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

75
  /// Constructing DeviceManagers is cheap; they only do expensive work if some
76
  /// of their methods are called.
77
  List<DeviceDiscovery> get deviceDiscoverers;
78

79
  String? _specifiedDeviceId;
80

81
  /// A user-specified device ID.
82
  String? get specifiedDeviceId {
83
    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') {
84
      return null;
85
    }
86 87
    return _specifiedDeviceId;
  }
88

89
  set specifiedDeviceId(String? id) {
90 91 92 93
    _specifiedDeviceId = id;
  }

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

96 97 98 99
  /// True when the user has specified all devices by setting
  /// specifiedDeviceId = 'all'.
  bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';

100
  Future<List<Device>> getDevicesById(String deviceId) async {
101
    final String lowerDeviceId = deviceId.toLowerCase();
102
    bool exactlyMatchesDeviceId(Device device) =>
103 104
        device.id.toLowerCase() == lowerDeviceId ||
        device.name.toLowerCase() == lowerDeviceId;
105
    bool startsWithDeviceId(Device device) =>
106 107 108 109 110
        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.
111 112 113 114 115 116
    // If an ID was specified, first check if it was a "well-known" device id.
    final Set<String> wellKnownIds = _platformDiscoverers
      .expand((DeviceDiscovery discovery) => discovery.wellKnownIds)
      .toSet();
    final bool hasWellKnownId = hasSpecifiedDeviceId && wellKnownIds.contains(specifiedDeviceId);

117 118 119 120
    // 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>();
121
    final List<Future<List<Device>?>> futureDevices = <Future<List<Device>?>>[
122
      for (final DeviceDiscovery discoverer in _platformDiscoverers)
123 124 125 126 127 128 129 130 131 132 133 134
        if (!hasWellKnownId || discoverer.wellKnownIds.contains(specifiedDeviceId))
          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);
              }
135
            }
136 137 138 139 140
            return null;
          }, onError: (dynamic error, StackTrace stackTrace) {
            // Return matches from other discoverers even if one fails.
            _logger.printTrace('Ignored error discovering $deviceId: $error');
          })
141 142 143
    ];

    // Wait for an exact match, or for all discoverers to return results.
144
    await Future.any<Object>(<Future<Object>>[
145
      exactMatchCompleter.future,
146
      Future.wait<List<Device>?>(futureDevices),
147 148 149 150
    ]);

    if (exactMatchCompleter.isCompleted) {
      return <Device>[await exactMatchCompleter.future];
151
    }
152
    return prefixMatches;
Devon Carew's avatar
Devon Carew committed
153 154
  }

155
  /// Returns the list of connected devices, filtered by any user-specified device id.
156
  Future<List<Device>> getDevices() {
157 158 159 160 161
    final String? id = specifiedDeviceId;
    if (id == null) {
      return getAllConnectedDevices();
    }
    return getDevicesById(id);
162 163
  }

164
  Iterable<DeviceDiscovery> get _platformDiscoverers {
165
    return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform);
166 167
  }

168
  /// Returns the list of all connected devices.
169 170 171 172 173 174 175
  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();
176
  }
177

178
  /// Returns the list of all connected devices. Discards existing cache of devices.
179
  Future<List<Device>> refreshAllConnectedDevices({ Duration? timeout }) async {
180 181 182 183 184 185 186 187
    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();
  }

188 189 190 191 192 193 194
  /// 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 {
195
    return <String>[
196
      for (final DeviceDiscovery discoverer in _platformDiscoverers)
197 198
        ...await discoverer.getDiagnostics(),
    ];
199
  }
200 201 202

  /// Find and return a list of devices based on the current project and environment.
  ///
203
  /// Returns a list of devices specified by the user.
204 205 206 207 208 209 210 211 212 213
  ///
  /// * 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.
214 215 216
  ///
  /// * If [flutterProject] is null, then assume the project supports all
  /// device types.
217
  Future<List<Device>> findTargetDevices(FlutterProject flutterProject, { Duration? timeout }) async {
218 219 220 221 222
    if (timeout != null) {
      // Reset the cache with the specified timeout.
      await refreshAllConnectedDevices(timeout: timeout);
    }

223
    List<Device> devices = await getDevices();
224

225 226
    // Always remove web and fuchsia devices from `--all`. This setting
    // currently requires devices to share a frontend_server and resident
227
    // runner instance. Both web and fuchsia require differently configured
228 229 230
    // compilers, and web requires an entirely different resident runner.
    if (hasSpecifiedAllDevices) {
      devices = <Device>[
231
        for (final Device device in devices)
232 233
          if (await device.targetPlatform != TargetPlatform.fuchsia_arm64 &&
              await device.targetPlatform != TargetPlatform.fuchsia_x64 &&
234
              await device.targetPlatform != TargetPlatform.web_javascript)
235
            device,
236 237 238 239 240 241 242 243
      ];
    }

    // 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>[
244
        for (final Device device in devices)
245
          if (isDeviceSupportedForProject(device, flutterProject))
246
            device,
247
      ];
248 249 250 251 252 253
    } 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>[];
254
    }
255

256 257
    // 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
258
    // user only typed 'flutter run' and both an Android device and desktop
259
    // device are available, choose the Android device.
260
    if (devices.length > 1 && !hasSpecifiedAllDevices) {
261 262 263
      // Note: ephemeral is nullable for device types where this is not well
      // defined.
      if (devices.any((Device device) => device.ephemeral == true)) {
264 265
        // if there is only one ephemeral device, get it
        final List<Device> ephemeralDevices = devices
266 267
            .where((Device device) => device.ephemeral == true)
            .toList();
268

269
            if (ephemeralDevices.length == 1) {
270 271 272 273 274 275 276
              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.
277 278 279
      if (devices.length > 1 && _terminal.stdinHasTerminal) {
        _logger.printStatus(_userMessages.flutterMultipleDevicesFound);
        await Device.printDevices(devices, _logger);
280
        final Device chosenDevice = await _chooseOneOfAvailableDevices(devices);
281
        specifiedDeviceId = chosenDevice.id;
282
        devices = <Device>[chosenDevice];
283 284 285 286 287
      }
    }
    return devices;
  }

288 289 290
  Future<Device> _chooseOneOfAvailableDevices(List<Device> devices) async {
    _displayDeviceOptions(devices);
    final String userInput =  await _readUserInput(devices.length);
291 292 293
    if (userInput.toLowerCase() == 'q') {
      throwToolExit('');
    }
294
    return devices[int.parse(userInput) - 1];
295 296 297
  }

  void _displayDeviceOptions(List<Device> devices) {
298
    int count = 1;
299
    for (final Device device in devices) {
300
      _logger.printStatus(_userMessages.flutterChooseDevice(count, device.name, device.id));
301 302 303 304 305
      count++;
    }
  }

  Future<String> _readUserInput(int deviceCount) async {
306 307
    _terminal.usesTerminalUi = true;
    final String result = await _terminal.promptForCharInput(
308
      <String>[ for (int i = 0; i < deviceCount; i++) '${i + 1}', 'q', 'Q'],
309 310 311 312
      displayAcceptedCharacters: false,
      logger: _logger,
      prompt: _userMessages.flutterChooseOne,
    );
313 314 315
    return result;
  }

316
  /// Returns whether the device is supported for the project.
317
  ///
318 319
  /// This exists to allow the check to be overridden for google3 clients. If
  /// [flutterProject] is null then return true.
320
  bool isDeviceSupportedForProject(Device device, FlutterProject flutterProject) {
321 322 323
    if (flutterProject == null) {
      return true;
    }
324 325
    return device.isSupportedForProject(flutterProject);
  }
326 327
}

328

329 330 331
/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
  bool get supportsPlatform;
332 333 334 335 336

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

337
  /// Return all connected devices, cached on subsequent calls.
338
  Future<List<Device>> get devices;
339

340
  /// Return all connected devices. Discards existing cache of devices.
341
  Future<List<Device>> discoverDevices({ Duration? timeout });
342

343 344
  /// Gets a list of diagnostic messages pertaining to issues with any connected
  /// devices (will be an empty list if there are no issues).
345
  Future<List<String>> getDiagnostics() => Future<List<String>>.value(<String>[]);
346 347 348 349 350 351 352 353 354

  /// Hard-coded device IDs that the discoverer can produce.
  ///
  /// These values are used by the device discovery to determine if it can
  /// short-circuit the other detectors if a specific ID is provided. If a
  /// discoverer has no valid fixed IDs, these should be left empty.
  ///
  /// For example, 'windows' or 'linux'.
  List<String> get wellKnownIds;
355 356
}

357 358 359 360 361
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
/// and removals.
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
  PollingDeviceDiscovery(this.name);

362 363
  static const Duration _pollingInterval = Duration(seconds: 4);
  static const Duration _pollingTimeout = Duration(seconds: 30);
364 365

  final String name;
366 367 368

  @protected
  @visibleForTesting
369
  ItemListNotifier<Device>? deviceNotifier;
370

371
  Timer? _timer;
372

373
  Future<List<Device>> pollingGetDevices({ Duration? timeout });
374

375
  void startPolling() {
376
    if (_timer == null) {
377
      deviceNotifier ??= ItemListNotifier<Device>();
378 379
      // Make initial population the default, fast polling timeout.
      _timer = _initTimer(null);
380 381 382
    }
  }

383
  Timer _initTimer(Duration? pollingTimeout) {
384 385
    return Timer(_pollingInterval, () async {
      try {
386
        final List<Device> devices = await pollingGetDevices(timeout: pollingTimeout);
387
        deviceNotifier!.updateWithNewList(devices);
388
      } on TimeoutException {
389
        // Do nothing on a timeout.
390
      }
391 392
      // Subsequent timeouts after initial population should wait longer.
      _timer = _initTimer(_pollingTimeout);
393 394 395
    });
  }

396
  void stopPolling() {
397 398
    _timer?.cancel();
    _timer = null;
399 400
  }

401
  @override
402
  Future<List<Device>> get devices {
403 404 405 406
    return _populateDevices();
  }

  @override
407
  Future<List<Device>> discoverDevices({ Duration? timeout }) {
408
    deviceNotifier = null;
409 410 411
    return _populateDevices(timeout: timeout);
  }

412
  Future<List<Device>> _populateDevices({ Duration? timeout }) async {
413
    deviceNotifier ??= ItemListNotifier<Device>.from(await pollingGetDevices(timeout: timeout));
414
    return deviceNotifier!.items;
415 416 417
  }

  Stream<Device> get onAdded {
418
    deviceNotifier ??= ItemListNotifier<Device>();
419
    return deviceNotifier!.onAdded;
420 421 422
  }

  Stream<Device> get onRemoved {
423
    deviceNotifier ??= ItemListNotifier<Device>();
424
    return deviceNotifier!.onRemoved;
425 426
  }

427
  void dispose() => stopPolling();
428

429
  @override
430 431 432
  String toString() => '$name device discovery';
}

433 434 435 436
/// 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.
437
abstract class Device {
438
  Device(this.id, {
439 440 441
    required this.category,
    required this.platformType,
    required this.ephemeral,
442
  });
443

444
  final String id;
445

446 447 448 449 450 451 452 453 454
  /// 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;

455 456
  String get name;

457 458
  bool get supportsStartPaused => true;

459
  /// Whether it is an emulated device running on localhost.
460 461 462
  ///
  /// This may return `true` for certain physical Android devices, and is
  /// generally only a best effort guess.
463
  Future<bool> get isLocalEmulator;
464

465 466 467 468 469 470 471 472
  /// 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;

473 474 475 476 477 478
  /// 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;

479
  /// Whether the device is a simulator on a platform which supports hardware rendering.
480
  // This is soft-deprecated since the logic is not correct expect for iOS simulators.
481
  Future<bool> get supportsHardwareRendering async {
482
    return true;
483 484
  }

485 486 487
  /// Whether the device is supported for the current project directory.
  bool isSupportedForProject(FlutterProject flutterProject);

488 489 490 491 492 493 494
  /// 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,
  });
495

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

499 500 501 502 503 504 505
  /// 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,
  });
506

507 508 509 510 511 512 513 514
  /// 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,
  });
515

516
  /// Check if the device is supported by Flutter.
517 518
  bool isSupported();

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

523 524
  /// The device's platform.
  Future<TargetPlatform> get targetPlatform;
525

526 527 528 529
  /// Platform name for display only.
  Future<String> get targetPlatformDisplayName async =>
      getNameForTargetPlatform(await targetPlatform);

530
  Future<String> get sdkNameAndVersion;
531

532 533 534
  /// Create a platform-specific [DevFSWriter] for the given [app], or
  /// null if the device does not support them.
  ///
535
  /// For example, the desktop device classes can use a writer which
536
  /// copies the files across the local file system.
537
  DevFSWriter? createDevFSWriter(
538 539 540 541 542 543
    covariant ApplicationPackage app,
    String userIdentifier,
  ) {
    return null;
  }

544
  /// Get a log reader for this device.
545 546
  ///
  /// If `app` is specified, this will return a log reader specific to that
547
  /// application. Otherwise, a global log reader will be returned.
548 549 550 551 552 553 554 555
  ///
  /// 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,
  });
556

557 558 559
  /// Get the port forwarder for this device.
  DevicePortForwarder get portForwarder;

560
  /// Get the DDS instance for this device.
561
  final DartDevelopmentService dds = DartDevelopmentService();
562

563 564
  /// Clear the device's logs.
  void clearLogs();
565

566
  /// Optional device-specific artifact overrides.
567
  OverrideArtifacts? get artifactOverrides => null;
568

569 570 571
  /// Start an app package on the current device.
  ///
  /// [platformArgs] allows callers to pass platform-specific arguments to the
572
  /// start call. The build mode is not used by all platforms.
Devon Carew's avatar
Devon Carew committed
573
  Future<LaunchResult> startApp(
574
    covariant ApplicationPackage package, {
575 576
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
577
    DebuggingOptions debuggingOptions,
578
    Map<String, Object?> platformArgs,
579 580
    bool prebuiltApplication = false,
    bool ipv6 = false,
581
    String userIdentifier,
582
  });
583

584 585 586 587 588
  /// Whether this device implements support for hot reload.
  bool get supportsHotReload => true;

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

590
  /// Whether flutter applications running on this device can be terminated
591
  /// from the VM Service.
592
  bool get supportsFlutterExit => true;
593

594 595
  /// Whether the device supports taking screenshots of a running flutter
  /// application.
Devon Carew's avatar
Devon Carew committed
596 597
  bool get supportsScreenshot => false;

598 599 600
  /// Whether the device supports the '--fast-start' development mode.
  bool get supportsFastStart => false;

601
  /// Stop an app package on the current device.
602 603 604 605 606 607
  ///
  /// Specify [userIdentifier] to stop app installed to a profile (Android only).
  Future<bool> stopApp(
    covariant ApplicationPackage app, {
    String userIdentifier,
  });
608

609 610 611 612 613 614 615 616
  /// 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());
  }

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

619
  @nonVirtual
620
  @override
621
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
622 623
  int get hashCode => id.hashCode;

624
  @nonVirtual
625
  @override
626
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
627
  bool operator ==(Object other) {
628
    if (identical(this, other)) {
629
      return true;
630
    }
631 632
    return other is Device
        && other.id == id;
633 634
  }

635
  @override
636
  String toString() => name;
Devon Carew's avatar
Devon Carew committed
637

638
  static Stream<String> descriptions(List<Device> devices) async* {
639
    if (devices.isEmpty) {
640
      return;
641
    }
Devon Carew's avatar
Devon Carew committed
642

643
    // Extract device information
644
    final List<List<String>> table = <List<String>>[];
645
    for (final Device device in devices) {
Devon Carew's avatar
Devon Carew committed
646
      String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
647 648 649
      final TargetPlatform targetPlatform = await device.targetPlatform;
      if (await device.isLocalEmulator) {
        final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
650 651
        supportIndicator += ' ($type)';
      }
652
      table.add(<String>[
653
        '${device.name} (${device.category})',
654
        device.id,
655
        await device.targetPlatformDisplayName,
656
        '${await device.sdkNameAndVersion}$supportIndicator',
657 658 659 660
      ]);
    }

    // Calculate column widths
661
    final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i);
662
    List<int> widths = indices.map<int>((int i) => 0).toList();
663
    for (final List<String> row in table) {
664
      widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
665 666 667
    }

    // Join columns into lines of text
668
    for (final List<String> row in table) {
669
      yield indices.map<String>((int i) => row[i].padRight(widths[i])).followedBy(<String>[row.last]).join(' • ');
670
    }
671 672
  }

673 674
  static Future<void> printDevices(List<Device> devices, Logger logger) async {
    await descriptions(devices).forEach(logger.printStatus);
Devon Carew's avatar
Devon Carew committed
675
  }
676

677 678 679 680 681 682 683
  static List<String> devicesPlatformTypes(List<Device> devices) {
    return devices
        .map(
          (Device d) => d.platformType.toString(),
        ).toSet().toList()..sort();
  }

684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705
  /// 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,
      }
    };
  }

706
  /// Clean up resources allocated by device.
707 708
  ///
  /// For example log readers or port forwarders.
709
  Future<void> dispose();
710 711
}

712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
/// 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
731
class DebuggingOptions {
732 733
  DebuggingOptions.enabled(
    this.buildInfo, {
734
    this.startPaused = false,
735
    this.disableServiceAuthCodes = false,
736
    this.enableDds = true,
737
    this.dartEntrypointArgs = const <String>[],
738
    this.dartFlags = '',
739 740 741
    this.enableSoftwareRendering = false,
    this.skiaDeterministicRendering = false,
    this.traceSkia = false,
742
    this.traceAllowlist,
743
    this.traceSkiaAllowlist,
744
    this.traceSystrace = false,
745
    this.endlessTraceBuffer = false,
746
    this.dumpSkpOnShaderCompilation = false,
747
    this.cacheSkSL = false,
748
    this.purgePersistentCache = false,
749
    this.useTestFonts = false,
750
    this.verboseSystemLogs = false,
751
    this.hostVmServicePort,
752
    this.disablePortPublication = false,
753
    this.deviceVmServicePort,
754
    this.ddsPort,
755
    this.devToolsServerAddress,
756 757
    this.hostname,
    this.port,
758
    this.webEnableExposeUrl,
759
    this.webUseSseForDebugProxy = true,
760
    this.webUseSseForDebugBackend = true,
761
    this.webUseSseForInjectedClient = true,
762 763
    this.webRunHeadless = false,
    this.webBrowserDebugPort,
764
    this.webEnableExpressionEvaluation = false,
765
    this.vmserviceOutFile,
766
    this.fastStart = false,
767
    this.nullAssertions = false,
768
    this.nativeNullAssertions = false,
Devon Carew's avatar
Devon Carew committed
769 770
   }) : debuggingEnabled = true;

771
  DebuggingOptions.disabled(this.buildInfo, {
772
      this.dartEntrypointArgs = const <String>[],
773 774 775
      this.port,
      this.hostname,
      this.webEnableExposeUrl,
776
      this.webUseSseForDebugProxy = true,
777
      this.webUseSseForDebugBackend = true,
778
      this.webUseSseForInjectedClient = true,
779 780
      this.webRunHeadless = false,
      this.webBrowserDebugPort,
781
      this.cacheSkSL = false,
782
      this.traceAllowlist,
783
    }) : debuggingEnabled = false,
784 785
      useTestFonts = false,
      startPaused = false,
786
      dartFlags = '',
787
      disableServiceAuthCodes = false,
788
      enableDds = true,
789 790 791
      enableSoftwareRendering = false,
      skiaDeterministicRendering = false,
      traceSkia = false,
792
      traceSkiaAllowlist = null,
793
      traceSystrace = false,
794
      endlessTraceBuffer = false,
795
      dumpSkpOnShaderCompilation = false,
796
      purgePersistentCache = false,
797
      verboseSystemLogs = false,
798
      hostVmServicePort = null,
799
      disablePortPublication = false,
800
      deviceVmServicePort = null,
801
      ddsPort = null,
802
      devToolsServerAddress = null,
803
      vmserviceOutFile = null,
804
      fastStart = false,
805
      webEnableExpressionEvaluation = false,
806 807
      nullAssertions = false,
      nativeNullAssertions = false;
Devon Carew's avatar
Devon Carew committed
808 809 810

  final bool debuggingEnabled;

811
  final BuildInfo buildInfo;
Devon Carew's avatar
Devon Carew committed
812
  final bool startPaused;
813
  final String dartFlags;
814
  final List<String> dartEntrypointArgs;
815
  final bool disableServiceAuthCodes;
816
  final bool enableDds;
817
  final bool enableSoftwareRendering;
818
  final bool skiaDeterministicRendering;
819
  final bool traceSkia;
820 821
  final String? traceAllowlist;
  final String? traceSkiaAllowlist;
822
  final bool traceSystrace;
823
  final bool endlessTraceBuffer;
824
  final bool dumpSkpOnShaderCompilation;
825
  final bool cacheSkSL;
826
  final bool purgePersistentCache;
827
  final bool useTestFonts;
828
  final bool verboseSystemLogs;
829 830
  final int? hostVmServicePort;
  final int? deviceVmServicePort;
831
  final bool disablePortPublication;
832 833 834 835 836
  final int? ddsPort;
  final Uri? devToolsServerAddress;
  final String? port;
  final String? hostname;
  final bool? webEnableExposeUrl;
837
  final bool webUseSseForDebugProxy;
838
  final bool webUseSseForDebugBackend;
839
  final bool webUseSseForInjectedClient;
840 841 842 843 844 845 846 847 848

  /// 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.
849
  final int? webBrowserDebugPort;
850

851
  /// Enable expression evaluation for web target.
852 853
  final bool webEnableExpressionEvaluation;

854
  /// A file where the VM Service URL should be written after the application is started.
855
  final String? vmserviceOutFile;
856
  final bool fastStart;
Devon Carew's avatar
Devon Carew committed
857

858 859
  final bool nullAssertions;

860 861 862
  /// Additional null runtime checks inserted for web applications.
  ///
  /// See also:
863
  ///   * https://github.com/dart-lang/sdk/blob/main/sdk/lib/html/doc/NATIVE_NULL_ASSERTIONS.md
864 865
  final bool nativeNullAssertions;

866
  bool get hasObservatoryPort => hostVmServicePort != null;
Devon Carew's avatar
Devon Carew committed
867 868 869
}

class LaunchResult {
870
  LaunchResult.succeeded({ this.observatoryUri }) : started = true;
871 872 873
  LaunchResult.failed()
    : started = false,
      observatoryUri = null;
Devon Carew's avatar
Devon Carew committed
874

875
  bool get hasObservatory => observatoryUri != null;
Devon Carew's avatar
Devon Carew committed
876 877

  final bool started;
878
  final Uri? observatoryUri;
Devon Carew's avatar
Devon Carew committed
879 880 881

  @override
  String toString() {
882
    final StringBuffer buf = StringBuffer('started=$started');
883
    if (observatoryUri != null) {
884
      buf.write(', observatory=$observatoryUri');
885
    }
Devon Carew's avatar
Devon Carew committed
886 887 888 889 890
    return buf.toString();
  }
}

/// Read the log for a particular device.
Devon Carew's avatar
Devon Carew committed
891 892 893
abstract class DeviceLogReader {
  String get name;

Devon Carew's avatar
Devon Carew committed
894 895
  /// 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
896

897 898
  /// Some logs can be obtained from a VM service stream.
  /// Set this after the VM services are connected.
899
  FlutterVmService? connectedVMService;
900

901
  @override
Devon Carew's avatar
Devon Carew committed
902
  String toString() => name;
903

904
  /// Process ID of the app on the device.
905
  int? appPid;
906 907

  // Clean up resources allocated by log reader e.g. subprocesses
908
  void dispose();
Devon Carew's avatar
Devon Carew committed
909
}
910 911 912

/// Describes an app running on the device.
class DiscoveredApp {
913
  DiscoveredApp(this.id, this.observatoryPort);
914 915 916
  final String id;
  final int observatoryPort;
}
917 918 919 920 921 922 923 924 925

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

  @override
  final String name;

  @override
926
  int? appPid;
927

928
  @override
929
  FlutterVmService? connectedVMService;
930

931 932
  @override
  Stream<String> get logLines => const Stream<String>.empty();
933 934 935

  @override
  void dispose() { }
936 937
}

938 939 940 941
/// Append --null_assertions to any existing Dart VM flags if
/// [debuggingOptions.nullAssertions] is true.
String computeDartVmFlags(DebuggingOptions debuggingOptions) {
  return <String>[
942
    if (debuggingOptions.dartFlags.isNotEmpty)
943 944 945 946 947
      debuggingOptions.dartFlags,
    if (debuggingOptions.nullAssertions)
      '--null_assertions',
  ].join(',');
}