device.dart 20.7 KB
Newer Older
1 2 3 4
// Copyright 2015 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.

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 'android/android_device.dart';
11
import 'application_package.dart';
12
import 'artifacts.dart';
13
import 'base/context.dart';
14
import 'base/file_system.dart';
15
import 'base/io.dart';
16
import 'base/utils.dart';
17
import 'build_info.dart';
18
import 'fuchsia/fuchsia_device.dart';
Devon Carew's avatar
Devon Carew committed
19
import 'globals.dart';
20 21
import 'ios/devices.dart';
import 'ios/simulators.dart';
22 23
import 'linux/linux_device.dart';
import 'macos/macos_device.dart';
24
import 'project.dart';
25
import 'tester/flutter_tester.dart';
26
import 'vmservice.dart';
27
import 'web/web_device.dart';
28
import 'windows/windows_device.dart';
29

30
DeviceManager get deviceManager => context.get<DeviceManager>();
31

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
/// 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;
}

64 65
/// A class to get all available devices.
class DeviceManager {
66

67
  /// Constructing DeviceManagers is cheap; they only do expensive work if some
68
  /// of their methods are called.
69 70 71 72 73 74 75
  List<DeviceDiscovery> get deviceDiscoverers => _deviceDiscoverers;
  final List<DeviceDiscovery> _deviceDiscoverers = List<DeviceDiscovery>.unmodifiable(<DeviceDiscovery>[
    AndroidDevices(),
    IOSDevices(),
    IOSSimulators(),
    FuchsiaDevices(),
    FlutterTesterDevices(),
76 77 78 79 80
    MacOSDevices(),
    LinuxDevices(),
    WindowsDevices(),
    WebDevices(),
  ]);
81

82 83
  String _specifiedDeviceId;

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

92 93 94 95 96
  set specifiedDeviceId(String id) {
    _specifiedDeviceId = id;
  }

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

99 100 101 102
  /// True when the user has specified all devices by setting
  /// specifiedDeviceId = 'all'.
  bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';

103
  Stream<Device> getDevicesById(String deviceId) async* {
104
    final List<Device> devices = await getAllConnectedDevices().toList();
Devon Carew's avatar
Devon Carew committed
105
    deviceId = deviceId.toLowerCase();
106 107 108 109 110 111 112
    bool exactlyMatchesDeviceId(Device device) =>
        device.id.toLowerCase() == deviceId ||
        device.name.toLowerCase() == deviceId;
    bool startsWithDeviceId(Device device) =>
        device.id.toLowerCase().startsWith(deviceId) ||
        device.name.toLowerCase().startsWith(deviceId);

113 114
    final Device exactMatch = devices.firstWhere(
        exactlyMatchesDeviceId, orElse: () => null);
115 116 117 118
    if (exactMatch != null) {
      yield exactMatch;
      return;
    }
119

120
    // Match on a id or name starting with [deviceId].
121
    for (Device device in devices.where(startsWithDeviceId)) {
122
      yield device;
123
    }
Devon Carew's avatar
Devon Carew committed
124 125
  }

126
  /// Return the list of connected devices, filtered by any user-specified device id.
127 128 129 130
  Stream<Device> getDevices() {
    return hasSpecifiedDeviceId
        ? getDevicesById(specifiedDeviceId)
        : getAllConnectedDevices();
131 132
  }

133
  Iterable<DeviceDiscovery> get _platformDiscoverers {
134
    return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform);
135 136
  }

137
  /// Return the list of all connected devices.
138 139 140 141 142 143
  Stream<Device> getAllConnectedDevices() async* {
    for (DeviceDiscovery discoverer in _platformDiscoverers) {
      for (Device device in await discoverer.devices) {
        yield device;
      }
    }
144
  }
145 146 147 148 149 150 151 152

  /// 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 {
153 154 155 156
    return <String>[
      for (DeviceDiscovery discoverer in _platformDiscoverers)
        ...await discoverer.getDiagnostics(),
    ];
157
  }
158 159 160

  /// Find and return a list of devices based on the current project and environment.
  ///
161 162 163 164 165 166 167 168 169 170 171
  /// Returns a list of deviecs specified by the user.
  ///
  /// * 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.
172 173 174
  Future<List<Device>> findTargetDevices(FlutterProject flutterProject) async {
    List<Device> devices = await getDevices().toList();

175 176 177 178 179 180 181
    // Always remove web and fuchsia devices from `--all`. This setting
    // currently requires devices to share a frontend_server and resident
    // runnner instance. Both web and fuchsia require differently configured
    // compilers, and web requires an entirely different resident runner.
    if (hasSpecifiedAllDevices) {
      devices = <Device>[
        for (Device device in devices)
182 183
          if (await device.targetPlatform != TargetPlatform.fuchsia_arm64 &&
              await device.targetPlatform != TargetPlatform.fuchsia_x64 &&
184
              await device.targetPlatform != TargetPlatform.web_javascript)
185
            device,
186 187 188 189 190 191 192 193 194
      ];
    }

    // 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>[
        for (Device device in devices)
195
          if (isDeviceSupportedForProject(device, flutterProject))
196
            device,
197
      ];
198 199 200 201 202 203
    } 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>[];
204
    }
205

206 207 208 209 210
    // 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
    // use only typed 'flutter run' and both an Android device and desktop
    // device are availible, choose the Android device.
    if (devices.length > 1 && !hasSpecifiedAllDevices) {
211 212 213 214 215 216 217 218 219 220 221 222
      // Note: ephemeral is nullable for device types where this is not well
      // defined.
      if (devices.any((Device device) => device.ephemeral == true)) {
        devices = devices
            .where((Device device) => device.ephemeral == true)
            .toList();
      }
    }
    return devices;
  }

  /// Returns whether the device is supported for the project.
223 224
  ///
  /// This exists to allow the check to be overriden for google3 clients.
225 226 227
  bool isDeviceSupportedForProject(Device device, FlutterProject flutterProject) {
    return device.isSupportedForProject(flutterProject);
  }
228 229 230 231 232
}

/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
  bool get supportsPlatform;
233 234 235 236 237

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

238
  Future<List<Device>> get devices;
239 240 241

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

245 246 247 248 249
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
/// and removals.
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
  PollingDeviceDiscovery(this.name);

250 251
  static const Duration _pollingInterval = Duration(seconds: 4);
  static const Duration _pollingTimeout = Duration(seconds: 30);
252 253 254

  final String name;
  ItemListNotifier<Device> _items;
255
  Timer _timer;
256

257
  Future<List<Device>> pollingGetDevices();
258 259

  void startPolling() {
260
    if (_timer == null) {
261
      _items ??= ItemListNotifier<Device>();
262
      _timer = _initTimer();
263 264 265
    }
  }

266 267 268 269 270 271 272 273 274 275 276 277
  Timer _initTimer() {
    return Timer(_pollingInterval, () async {
      try {
        final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
        _items.updateWithNewList(devices);
      } on TimeoutException {
        printTrace('Device poll timed out. Will retry.');
      }
      _timer = _initTimer();
    });
  }

278
  void stopPolling() {
279 280
    _timer?.cancel();
    _timer = null;
281 282
  }

283
  @override
284
  Future<List<Device>> get devices async {
285
    _items ??= ItemListNotifier<Device>.from(await pollingGetDevices());
286 287 288 289
    return _items.items;
  }

  Stream<Device> get onAdded {
290
    _items ??= ItemListNotifier<Device>();
291 292 293 294
    return _items.onAdded;
  }

  Stream<Device> get onRemoved {
295
    _items ??= ItemListNotifier<Device>();
296 297 298 299 300
    return _items.onRemoved;
  }

  void dispose() => stopPolling();

301
  @override
302 303 304
  String toString() => '$name device discovery';
}

305
abstract class Device {
306
  Device(this.id, {@required this.category, @required this.platformType, @required this.ephemeral});
307

308
  final String id;
309

310 311 312 313 314 315 316 317 318
  /// 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;

319 320
  String get name;

321 322
  bool get supportsStartPaused => true;

323
  /// Whether it is an emulated device running on localhost.
324
  Future<bool> get isLocalEmulator;
325

326 327 328 329 330 331 332 333
  /// 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;

334 335 336 337 338 339 340 341 342 343 344 345 346
  /// Whether the device is a simulator on a platform which supports hardware rendering.
  Future<bool> get supportsHardwareRendering async {
    assert(await isLocalEmulator);
    switch (await targetPlatform) {
      case TargetPlatform.android_arm:
      case TargetPlatform.android_arm64:
      case TargetPlatform.android_x64:
      case TargetPlatform.android_x86:
        return true;
      case TargetPlatform.ios:
      case TargetPlatform.darwin_x64:
      case TargetPlatform.linux_x64:
      case TargetPlatform.windows_x64:
347 348
      case TargetPlatform.fuchsia_arm64:
      case TargetPlatform.fuchsia_x64:
349 350 351 352 353
      default:
        return false;
    }
  }

354 355 356
  /// Whether the device is supported for the current project directory.
  bool isSupportedForProject(FlutterProject flutterProject);

357
  /// Check if a version of the given app is already installed
358
  Future<bool> isAppInstalled(covariant ApplicationPackage app);
359

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

363
  /// Install an app package on the current device
364
  Future<bool> installApp(covariant ApplicationPackage app);
365

366
  /// Uninstall an app package from the current device
367
  Future<bool> uninstallApp(covariant ApplicationPackage app);
368

369 370 371 372 373
  /// Check if the device is supported by Flutter
  bool isSupported();

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

376 377
  /// The device's platform.
  Future<TargetPlatform> get targetPlatform;
378

379
  Future<String> get sdkNameAndVersion;
380

381 382 383
  /// Get a log reader for this device.
  /// If [app] is specified, this will return a log reader specific to that
  /// application. Otherwise, a global log reader will be returned.
384
  DeviceLogReader getLogReader({ covariant ApplicationPackage app });
385

386 387 388
  /// Get the port forwarder for this device.
  DevicePortForwarder get portForwarder;

389 390
  /// Clear the device's logs.
  void clearLogs();
391

392 393 394
  /// Optional device-specific artifact overrides.
  OverrideArtifacts get artifactOverrides => null;

395 396 397
  /// Start an app package on the current device.
  ///
  /// [platformArgs] allows callers to pass platform-specific arguments to the
398
  /// start call. The build mode is not used by all platforms.
Devon Carew's avatar
Devon Carew committed
399
  Future<LaunchResult> startApp(
400
    covariant ApplicationPackage package, {
401 402
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
403
    DebuggingOptions debuggingOptions,
404
    Map<String, dynamic> platformArgs,
405 406
    bool prebuiltApplication = false,
    bool ipv6 = false,
407
  });
408

409 410 411 412 413
  /// Whether this device implements support for hot reload.
  bool get supportsHotReload => true;

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

415 416
  /// Whether flutter applications running on this device can be terminated
  /// from the vmservice.
417
  bool get supportsFlutterExit => true;
418

419 420
  /// Whether the device supports taking screenshots of a running flutter
  /// application.
Devon Carew's avatar
Devon Carew committed
421 422
  bool get supportsScreenshot => false;

423
  /// Stop an app package on the current device.
424
  Future<bool> stopApp(covariant ApplicationPackage app);
425

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

428
  @override
429 430
  int get hashCode => id.hashCode;

431
  @override
432
  bool operator ==(dynamic other) {
433
    if (identical(this, other)) {
434
      return true;
435 436
    }
    if (other is! Device) {
437
      return false;
438
    }
439 440 441
    return id == other.id;
  }

442
  @override
443
  String toString() => name;
Devon Carew's avatar
Devon Carew committed
444

445
  static Stream<String> descriptions(List<Device> devices) async* {
446
    if (devices.isEmpty) {
447
      return;
448
    }
Devon Carew's avatar
Devon Carew committed
449

450
    // Extract device information
451
    final List<List<String>> table = <List<String>>[];
Devon Carew's avatar
Devon Carew committed
452 453
    for (Device device in devices) {
      String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
454 455 456
      final TargetPlatform targetPlatform = await device.targetPlatform;
      if (await device.isLocalEmulator) {
        final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
457 458
        supportIndicator += ' ($type)';
      }
459 460 461
      table.add(<String>[
        device.name,
        device.id,
462 463
        '${getNameForTargetPlatform(targetPlatform)}',
        '${await device.sdkNameAndVersion}$supportIndicator',
464 465 466 467
      ]);
    }

    // Calculate column widths
468
    final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i);
469
    List<int> widths = indices.map<int>((int i) => 0).toList();
470
    for (List<String> row in table) {
471
      widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
472 473 474
    }

    // Join columns into lines of text
475
    for (List<String> row in table) {
476
      yield indices.map<String>((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
477
    }
478 479
  }

480
  static Future<void> printDevices(List<Device> devices) async {
481
    await descriptions(devices).forEach(printStatus);
Devon Carew's avatar
Devon Carew committed
482
  }
483 484 485 486 487

  /// Clean up resources allocated by device
  ///
  /// For example log readers or port forwarders.
  void dispose() {}
488 489
}

Devon Carew's avatar
Devon Carew committed
490
class DebuggingOptions {
491 492
  DebuggingOptions.enabled(
    this.buildInfo, {
493
    this.startPaused = false,
494
    this.disableServiceAuthCodes = false,
495
    this.dartFlags = '',
496 497 498
    this.enableSoftwareRendering = false,
    this.skiaDeterministicRendering = false,
    this.traceSkia = false,
499
    this.traceSystrace = false,
500
    this.dumpSkpOnShaderCompilation = false,
501
    this.cacheSkSL = false,
502
    this.useTestFonts = false,
503
    this.verboseSystemLogs = false,
504 505
    this.hostVmServicePort,
    this.deviceVmServicePort,
506
    this.initializePlatform = true,
507 508
    this.hostname,
    this.port,
509
    this.vmserviceOutFile,
Devon Carew's avatar
Devon Carew committed
510 511
   }) : debuggingEnabled = true;

512
  DebuggingOptions.disabled(this.buildInfo, { this.initializePlatform = true, this.port, this.hostname, this.cacheSkSL = false, })
513 514 515
    : debuggingEnabled = false,
      useTestFonts = false,
      startPaused = false,
516
      dartFlags = '',
517
      disableServiceAuthCodes = false,
518 519 520 521
      enableSoftwareRendering = false,
      skiaDeterministicRendering = false,
      traceSkia = false,
      traceSystrace = false,
522
      dumpSkpOnShaderCompilation = false,
523
      verboseSystemLogs = false,
524 525
      hostVmServicePort = null,
      deviceVmServicePort = null,
526
      vmserviceOutFile = null;
Devon Carew's avatar
Devon Carew committed
527 528 529

  final bool debuggingEnabled;

530
  final BuildInfo buildInfo;
Devon Carew's avatar
Devon Carew committed
531
  final bool startPaused;
532
  final String dartFlags;
533
  final bool disableServiceAuthCodes;
534
  final bool enableSoftwareRendering;
535
  final bool skiaDeterministicRendering;
536
  final bool traceSkia;
537
  final bool traceSystrace;
538
  final bool dumpSkpOnShaderCompilation;
539
  final bool cacheSkSL;
540
  final bool useTestFonts;
541
  final bool verboseSystemLogs;
542 543
  /// Whether to invoke webOnlyInitializePlatform in Flutter for web.
  final bool initializePlatform;
544 545
  final int hostVmServicePort;
  final int deviceVmServicePort;
546 547
  final String port;
  final String hostname;
548 549
  /// A file where the vmservice uri should be written after the application is started.
  final String vmserviceOutFile;
Devon Carew's avatar
Devon Carew committed
550

551
  bool get hasObservatoryPort => hostVmServicePort != null;
Devon Carew's avatar
Devon Carew committed
552 553 554
}

class LaunchResult {
555
  LaunchResult.succeeded({ this.observatoryUri }) : started = true;
556 557 558
  LaunchResult.failed()
    : started = false,
      observatoryUri = null;
Devon Carew's avatar
Devon Carew committed
559

560
  bool get hasObservatory => observatoryUri != null;
Devon Carew's avatar
Devon Carew committed
561 562

  final bool started;
563
  final Uri observatoryUri;
Devon Carew's avatar
Devon Carew committed
564 565 566

  @override
  String toString() {
567
    final StringBuffer buf = StringBuffer('started=$started');
568
    if (observatoryUri != null) {
569
      buf.write(', observatory=$observatoryUri');
570
    }
Devon Carew's avatar
Devon Carew committed
571 572 573 574
    return buf.toString();
  }
}

575
class ForwardedPort {
576 577
  ForwardedPort(this.hostPort, this.devicePort) : context = null;
  ForwardedPort.withContext(this.hostPort, this.devicePort, this.context);
578 579 580

  final int hostPort;
  final int devicePort;
581
  final Process context;
582

583
  @override
584
  String toString() => 'ForwardedPort HOST:$hostPort to DEVICE:$devicePort';
585 586 587

  /// Kill subprocess (if present) used in forwarding.
  void dispose() {
588 589
    if (context != null) {
      context.kill();
590 591
    }
  }
592 593 594 595 596 597 598 599 600
}

/// 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.
601
  /// If [hostPort] is null or zero, will auto select a host port.
602
  /// Returns a Future that completes with the host port.
603
  Future<int> forward(int devicePort, { int hostPort });
604 605

  /// Stops forwarding [forwardedPort].
606
  Future<void> unforward(ForwardedPort forwardedPort);
607 608 609

  /// Cleanup allocated resources, like forwardedPorts
  Future<void> dispose() async { }
610 611
}

Devon Carew's avatar
Devon Carew committed
612
/// Read the log for a particular device.
Devon Carew's avatar
Devon Carew committed
613 614 615
abstract class DeviceLogReader {
  String get name;

Devon Carew's avatar
Devon Carew committed
616 617
  /// 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
618

619 620 621 622
  /// Some logs can be obtained from a VM service stream.
  /// Set this after the VM services are connected.
  List<VMService> connectedVMServices;

623
  @override
Devon Carew's avatar
Devon Carew committed
624
  String toString() => name;
625

626
  /// Process ID of the app on the device.
627
  int appPid;
628 629 630

  // Clean up resources allocated by log reader e.g. subprocesses
  void dispose() { }
Devon Carew's avatar
Devon Carew committed
631
}
632 633 634

/// Describes an app running on the device.
class DiscoveredApp {
635
  DiscoveredApp(this.id, this.observatoryPort);
636 637 638
  final String id;
  final int observatoryPort;
}
639 640 641 642 643 644 645 646 647 648 649

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

  @override
  final String name;

  @override
  int appPid;

650 651 652
  @override
  List<VMService> connectedVMServices;

653 654
  @override
  Stream<String> get logLines => const Stream<String>.empty();
655 656 657

  @override
  void dispose() { }
658 659 660 661 662 663 664
}

// A portforwarder which does not support forwarding ports.
class NoOpDevicePortForwarder implements DevicePortForwarder {
  const NoOpDevicePortForwarder();

  @override
665
  Future<int> forward(int devicePort, { int hostPort }) async => devicePort;
666 667 668 669 670

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

  @override
671
  Future<void> unforward(ForwardedPort forwardedPort) async { }
672 673 674

  @override
  Future<void> dispose() async { }
675
}