1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../android/android_sdk.dart'; 10import '../android/android_workflow.dart'; 11import '../android/apk.dart'; 12import '../application_package.dart'; 13import '../base/common.dart' show throwToolExit; 14import '../base/file_system.dart'; 15import '../base/io.dart'; 16import '../base/logger.dart'; 17import '../base/platform.dart'; 18import '../base/process.dart'; 19import '../base/process_manager.dart'; 20import '../build_info.dart'; 21import '../convert.dart'; 22import '../device.dart'; 23import '../globals.dart'; 24import '../project.dart'; 25import '../protocol_discovery.dart'; 26 27import 'adb.dart'; 28import 'android.dart'; 29import 'android_console.dart'; 30import 'android_sdk.dart'; 31 32enum _HardwareType { emulator, physical } 33 34/// Map to help our `isLocalEmulator` detection. 35const Map<String, _HardwareType> _knownHardware = <String, _HardwareType>{ 36 'goldfish': _HardwareType.emulator, 37 'qcom': _HardwareType.physical, 38 'ranchu': _HardwareType.emulator, 39 'samsungexynos7420': _HardwareType.physical, 40 'samsungexynos7580': _HardwareType.physical, 41 'samsungexynos7870': _HardwareType.physical, 42 'samsungexynos8890': _HardwareType.physical, 43 'samsungexynos8895': _HardwareType.physical, 44 'samsungexynos9810': _HardwareType.physical, 45}; 46 47bool allowHeapCorruptionOnWindows(int exitCode) { 48 // In platform tools 29.0.0 adb.exe seems to be ending with this heap 49 // corruption error code on seemingly successful termination. 50 // So we ignore this error on Windows. 51 return exitCode == -1073740940 && platform.isWindows; 52} 53 54class AndroidDevices extends PollingDeviceDiscovery { 55 AndroidDevices() : super('Android devices'); 56 57 @override 58 bool get supportsPlatform => true; 59 60 @override 61 bool get canListAnything => androidWorkflow.canListDevices; 62 63 @override 64 Future<List<Device>> pollingGetDevices() async => getAdbDevices(); 65 66 @override 67 Future<List<String>> getDiagnostics() async => getAdbDeviceDiagnostics(); 68} 69 70class AndroidDevice extends Device { 71 AndroidDevice( 72 String id, { 73 this.productID, 74 this.modelID, 75 this.deviceCodeName, 76 }) : super( 77 id, 78 category: Category.mobile, 79 platformType: PlatformType.android, 80 ephemeral: true, 81 ); 82 83 final String productID; 84 final String modelID; 85 final String deviceCodeName; 86 87 Map<String, String> _properties; 88 bool _isLocalEmulator; 89 TargetPlatform _platform; 90 91 Future<String> _getProperty(String name) async { 92 if (_properties == null) { 93 _properties = <String, String>{}; 94 95 final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']); 96 printTrace(propCommand.join(' ')); 97 98 try { 99 // We pass an encoding of latin1 so that we don't try and interpret the 100 // `adb shell getprop` result as UTF8. 101 final ProcessResult result = await processManager.run( 102 propCommand, 103 stdoutEncoding: latin1, 104 stderrEncoding: latin1, 105 ); 106 if (result.exitCode == 0 || allowHeapCorruptionOnWindows(result.exitCode)) { 107 _properties = parseAdbDeviceProperties(result.stdout); 108 } else { 109 printError('Error ${result.exitCode} retrieving device properties for $name:'); 110 printError(result.stderr); 111 } 112 } on ProcessException catch (error) { 113 printError('Error retrieving device properties for $name: $error'); 114 } 115 } 116 117 return _properties[name]; 118 } 119 120 @override 121 Future<bool> get isLocalEmulator async { 122 if (_isLocalEmulator == null) { 123 final String hardware = await _getProperty('ro.hardware'); 124 printTrace('ro.hardware = $hardware'); 125 if (_knownHardware.containsKey(hardware)) { 126 // Look for known hardware models. 127 _isLocalEmulator = _knownHardware[hardware] == _HardwareType.emulator; 128 } else { 129 // Fall back to a best-effort heuristic-based approach. 130 final String characteristics = await _getProperty('ro.build.characteristics'); 131 printTrace('ro.build.characteristics = $characteristics'); 132 _isLocalEmulator = characteristics != null && characteristics.contains('emulator'); 133 } 134 } 135 return _isLocalEmulator; 136 } 137 138 /// The unique identifier for the emulator that corresponds to this device, or 139 /// null if it is not an emulator. 140 /// 141 /// The ID returned matches that in the output of `flutter emulators`. Fetching 142 /// this name may require connecting to the device and if an error occurs null 143 /// will be returned. 144 @override 145 Future<String> get emulatorId async { 146 if (!(await isLocalEmulator)) 147 return null; 148 149 // Emulators always have IDs in the format emulator-(port) where port is the 150 // Android Console port number. 151 final RegExp emulatorPortRegex = RegExp(r'emulator-(\d+)'); 152 153 final Match portMatch = emulatorPortRegex.firstMatch(id); 154 if (portMatch == null || portMatch.groupCount < 1) { 155 return null; 156 } 157 158 const String host = 'localhost'; 159 final int port = int.parse(portMatch.group(1)); 160 printTrace('Fetching avd name for $name via Android console on $host:$port'); 161 162 try { 163 final Socket socket = await androidConsoleSocketFactory(host, port); 164 final AndroidConsole console = AndroidConsole(socket); 165 166 try { 167 await console 168 .connect() 169 .timeout(timeoutConfiguration.fastOperation, 170 onTimeout: () => throw TimeoutException('Connection timed out')); 171 172 return await console 173 .getAvdName() 174 .timeout(timeoutConfiguration.fastOperation, 175 onTimeout: () => throw TimeoutException('"avd name" timed out')); 176 } finally { 177 console.destroy(); 178 } 179 } catch (e) { 180 printTrace('Failed to fetch avd name for emulator at $host:$port: $e'); 181 // If we fail to connect to the device, we should not fail so just return 182 // an empty name. This data is best-effort. 183 return null; 184 } 185 } 186 187 @override 188 Future<TargetPlatform> get targetPlatform async { 189 if (_platform == null) { 190 // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...) 191 switch (await _getProperty('ro.product.cpu.abi')) { 192 case 'arm64-v8a': 193 _platform = TargetPlatform.android_arm64; 194 break; 195 case 'x86_64': 196 _platform = TargetPlatform.android_x64; 197 break; 198 case 'x86': 199 _platform = TargetPlatform.android_x86; 200 break; 201 default: 202 _platform = TargetPlatform.android_arm; 203 break; 204 } 205 } 206 207 return _platform; 208 } 209 210 @override 211 Future<String> get sdkNameAndVersion async => 212 'Android ${await _sdkVersion} (API ${await _apiVersion})'; 213 214 Future<String> get _sdkVersion => _getProperty('ro.build.version.release'); 215 216 Future<String> get _apiVersion => _getProperty('ro.build.version.sdk'); 217 218 _AdbLogReader _logReader; 219 _AndroidDevicePortForwarder _portForwarder; 220 221 List<String> adbCommandForDevice(List<String> args) { 222 return <String>[getAdbPath(androidSdk), '-s', id, ...args]; 223 } 224 225 String runAdbCheckedSync( 226 List<String> params, { 227 String workingDirectory, 228 bool allowReentrantFlutter = false, 229 Map<String, String> environment}) { 230 return runCheckedSync(adbCommandForDevice(params), workingDirectory: workingDirectory, 231 allowReentrantFlutter: allowReentrantFlutter, 232 environment: environment, 233 whiteListFailures: allowHeapCorruptionOnWindows 234 ); 235 } 236 237 Future<RunResult> runAdbCheckedAsync( 238 List<String> params, { 239 String workingDirectory, 240 bool allowReentrantFlutter = false, 241 }) async { 242 return runCheckedAsync(adbCommandForDevice(params), workingDirectory: workingDirectory, 243 allowReentrantFlutter: allowReentrantFlutter, 244 whiteListFailures: allowHeapCorruptionOnWindows); 245 } 246 247 bool _isValidAdbVersion(String adbVersion) { 248 // Sample output: 'Android Debug Bridge version 1.0.31' 249 final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion); 250 if (versionFields != null) { 251 final int majorVersion = int.parse(versionFields[1]); 252 final int minorVersion = int.parse(versionFields[2]); 253 final int patchVersion = int.parse(versionFields[3]); 254 if (majorVersion > 1) { 255 return true; 256 } 257 if (majorVersion == 1 && minorVersion > 0) { 258 return true; 259 } 260 if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 39) { 261 return true; 262 } 263 return false; 264 } 265 printError( 266 'Unrecognized adb version string $adbVersion. Skipping version check.'); 267 return true; 268 } 269 270 Future<bool> _checkForSupportedAdbVersion() async { 271 if (androidSdk == null) 272 return false; 273 274 try { 275 final RunResult adbVersion = await runCheckedAsync(<String>[getAdbPath(androidSdk), 'version']); 276 if (_isValidAdbVersion(adbVersion.stdout)) 277 return true; 278 printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.39 or later.'); 279 } catch (error, trace) { 280 printError('Error running ADB: $error', stackTrace: trace); 281 } 282 283 return false; 284 } 285 286 Future<bool> _checkForSupportedAndroidVersion() async { 287 try { 288 // If the server is automatically restarted, then we get irrelevant 289 // output lines like this, which we want to ignore: 290 // adb server is out of date. killing.. 291 // * daemon started successfully * 292 await runCheckedAsync(<String>[getAdbPath(androidSdk), 'start-server']); 293 294 // Sample output: '22' 295 final String sdkVersion = await _getProperty('ro.build.version.sdk'); 296 297 298 final int sdkVersionParsed = int.tryParse(sdkVersion); 299 if (sdkVersionParsed == null) { 300 printError('Unexpected response from getprop: "$sdkVersion"'); 301 return false; 302 } 303 304 if (sdkVersionParsed < minApiLevel) { 305 printError( 306 'The Android version ($sdkVersion) on the target device is too old. Please ' 307 'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.'); 308 return false; 309 } 310 311 return true; 312 } catch (e) { 313 printError('Unexpected failure from adb: $e'); 314 return false; 315 } 316 } 317 318 String _getDeviceSha1Path(ApplicationPackage app) { 319 return '/data/local/tmp/sky.${app.id}.sha1'; 320 } 321 322 Future<String> _getDeviceApkSha1(ApplicationPackage app) async { 323 final RunResult result = await runAsync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)])); 324 return result.stdout; 325 } 326 327 String _getSourceSha1(ApplicationPackage app) { 328 final AndroidApk apk = app; 329 final File shaFile = fs.file('${apk.file.path}.sha1'); 330 return shaFile.existsSync() ? shaFile.readAsStringSync() : ''; 331 } 332 333 @override 334 String get name => modelID; 335 336 @override 337 Future<bool> isAppInstalled(ApplicationPackage app) async { 338 // This call takes 400ms - 600ms. 339 try { 340 final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]); 341 return LineSplitter.split(listOut.stdout).contains('package:${app.id}'); 342 } catch (error) { 343 printTrace('$error'); 344 return false; 345 } 346 } 347 348 @override 349 Future<bool> isLatestBuildInstalled(ApplicationPackage app) async { 350 final String installedSha1 = await _getDeviceApkSha1(app); 351 return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app); 352 } 353 354 @override 355 Future<bool> installApp(ApplicationPackage app) async { 356 final AndroidApk apk = app; 357 if (!apk.file.existsSync()) { 358 printError('"${fs.path.relative(apk.file.path)}" does not exist.'); 359 return false; 360 } 361 362 if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion()) 363 return false; 364 365 final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', timeout: timeoutConfiguration.slowOperation); 366 final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path])); 367 status.stop(); 368 // Some versions of adb exit with exit code 0 even on failure :( 369 // Parsing the output to check for failures. 370 final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true); 371 final String failure = failureExp.stringMatch(installResult.stdout); 372 if (failure != null) { 373 printError('Package install error: $failure'); 374 return false; 375 } 376 if (installResult.exitCode != 0) { 377 printError('Error: ADB exited with exit code ${installResult.exitCode}'); 378 printError('$installResult'); 379 return false; 380 } 381 try { 382 await runAdbCheckedAsync(<String>[ 383 'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app), 384 ]); 385 } on ProcessException catch (error) { 386 printError('adb shell failed to write the SHA hash: $error.'); 387 return false; 388 } 389 return true; 390 } 391 392 @override 393 Future<bool> uninstallApp(ApplicationPackage app) async { 394 if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion()) 395 return false; 396 397 String uninstallOut; 398 try { 399 uninstallOut = (await runCheckedAsync(adbCommandForDevice(<String>['uninstall', app.id]))).stdout; 400 } catch (error) { 401 printError('adb uninstall failed: $error'); 402 return false; 403 } 404 final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true); 405 final String failure = failureExp.stringMatch(uninstallOut); 406 if (failure != null) { 407 printError('Package uninstall error: $failure'); 408 return false; 409 } 410 411 return true; 412 } 413 414 Future<bool> _installLatestApp(ApplicationPackage package) async { 415 final bool wasInstalled = await isAppInstalled(package); 416 if (wasInstalled) { 417 if (await isLatestBuildInstalled(package)) { 418 printTrace('Latest build already installed.'); 419 return true; 420 } 421 } 422 printTrace('Installing APK.'); 423 if (!await installApp(package)) { 424 printTrace('Warning: Failed to install APK.'); 425 if (wasInstalled) { 426 printStatus('Uninstalling old version...'); 427 if (!await uninstallApp(package)) { 428 printError('Error: Uninstalling old version failed.'); 429 return false; 430 } 431 if (!await installApp(package)) { 432 printError('Error: Failed to install APK again.'); 433 return false; 434 } 435 return true; 436 } 437 return false; 438 } 439 return true; 440 } 441 442 @override 443 Future<LaunchResult> startApp( 444 ApplicationPackage package, { 445 String mainPath, 446 String route, 447 DebuggingOptions debuggingOptions, 448 Map<String, dynamic> platformArgs, 449 bool prebuiltApplication = false, 450 bool ipv6 = false, 451 bool usesTerminalUi = true, 452 }) async { 453 if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion()) 454 return LaunchResult.failed(); 455 456 final TargetPlatform devicePlatform = await targetPlatform; 457 if (!(devicePlatform == TargetPlatform.android_arm || 458 devicePlatform == TargetPlatform.android_arm64) && 459 !debuggingOptions.buildInfo.isDebug) { 460 printError('Profile and release builds are only supported on ARM targets.'); 461 return LaunchResult.failed(); 462 } 463 464 AndroidArch androidArch; 465 switch (devicePlatform) { 466 case TargetPlatform.android_arm: 467 androidArch = AndroidArch.armeabi_v7a; 468 break; 469 case TargetPlatform.android_arm64: 470 androidArch = AndroidArch.arm64_v8a; 471 break; 472 case TargetPlatform.android_x64: 473 androidArch = AndroidArch.x86_64; 474 break; 475 case TargetPlatform.android_x86: 476 androidArch = AndroidArch.x86; 477 break; 478 default: 479 printError('Android platforms are only supported.'); 480 return LaunchResult.failed(); 481 } 482 483 if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) { 484 printTrace('Building APK'); 485 final FlutterProject project = FlutterProject.current(); 486 await buildApk( 487 project: project, 488 target: mainPath, 489 androidBuildInfo: AndroidBuildInfo(debuggingOptions.buildInfo, 490 targetArchs: <AndroidArch>[androidArch] 491 ), 492 ); 493 // Package has been built, so we can get the updated application ID and 494 // activity name from the .apk. 495 package = await AndroidApk.fromAndroidProject(project.android); 496 } 497 // There was a failure parsing the android project information. 498 if (package == null) { 499 throwToolExit('Problem building Android application: see above error(s).'); 500 } 501 502 printTrace("Stopping app '${package.name}' on $name."); 503 await stopApp(package); 504 505 if (!await _installLatestApp(package)) 506 return LaunchResult.failed(); 507 508 final bool traceStartup = platformArgs['trace-startup'] ?? false; 509 final AndroidApk apk = package; 510 printTrace('$this startApp'); 511 512 ProtocolDiscovery observatoryDiscovery; 513 514 if (debuggingOptions.debuggingEnabled) { 515 // TODO(devoncarew): Remember the forwarding information (so we can later remove the 516 // port forwarding or set it up again when adb fails on us). 517 observatoryDiscovery = ProtocolDiscovery.observatory( 518 getLogReader(), 519 portForwarder: portForwarder, 520 hostPort: debuggingOptions.observatoryPort, 521 ipv6: ipv6, 522 ); 523 } 524 525 List<String> cmd; 526 527 cmd = <String>[ 528 'shell', 'am', 'start', 529 '-a', 'android.intent.action.RUN', 530 '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP 531 '--ez', 'enable-background-compilation', 'true', 532 '--ez', 'enable-dart-profiling', 'true', 533 if (traceStartup) 534 ...<String>['--ez', 'trace-startup', 'true'], 535 if (route != null) 536 ...<String>['--es', 'route', route], 537 if (debuggingOptions.enableSoftwareRendering) 538 ...<String>['--ez', 'enable-software-rendering', 'true'], 539 if (debuggingOptions.skiaDeterministicRendering) 540 ...<String>['--ez', 'skia-deterministic-rendering', 'true'], 541 if (debuggingOptions.traceSkia) 542 ...<String>['--ez', 'trace-skia', 'true'], 543 if (debuggingOptions.traceSystrace) 544 ...<String>['--ez', 'trace-systrace', 'true'], 545 if (debuggingOptions.dumpSkpOnShaderCompilation) 546 ...<String>['--ez', 'dump-skp-on-shader-compilation', 'true'], 547 if (debuggingOptions.debuggingEnabled) 548 ...<String>[ 549 if (debuggingOptions.buildInfo.isDebug) 550 ...<String>[ 551 ...<String>['--ez', 'enable-checked-mode', 'true'], 552 ...<String>['--ez', 'verify-entry-points', 'true'], 553 ], 554 if (debuggingOptions.startPaused) 555 ...<String>['--ez', 'start-paused', 'true'], 556 if (debuggingOptions.disableServiceAuthCodes) 557 ...<String>['--ez', 'disable-service-auth-codes', 'true'], 558 if (debuggingOptions.dartFlags.isNotEmpty) 559 ...<String>['--es', 'dart-flags', debuggingOptions.dartFlags], 560 if (debuggingOptions.useTestFonts) 561 ...<String>['--ez', 'use-test-fonts', 'true'], 562 if (debuggingOptions.verboseSystemLogs) 563 ...<String>['--ez', 'verbose-logging', 'true'], 564 ], 565 apk.launchActivity, 566 ]; 567 final String result = (await runAdbCheckedAsync(cmd)).stdout; 568 // This invocation returns 0 even when it fails. 569 if (result.contains('Error: ')) { 570 printError(result.trim(), wrap: false); 571 return LaunchResult.failed(); 572 } 573 574 if (!debuggingOptions.debuggingEnabled) 575 return LaunchResult.succeeded(); 576 577 // Wait for the service protocol port here. This will complete once the 578 // device has printed "Observatory is listening on...". 579 printTrace('Waiting for observatory port to be available...'); 580 581 // TODO(danrubel): Waiting for observatory services can be made common across all devices. 582 try { 583 Uri observatoryUri; 584 585 if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) { 586 observatoryUri = await observatoryDiscovery.uri; 587 } 588 589 return LaunchResult.succeeded(observatoryUri: observatoryUri); 590 } catch (error) { 591 printError('Error waiting for a debug connection: $error'); 592 return LaunchResult.failed(); 593 } finally { 594 await observatoryDiscovery.cancel(); 595 } 596 } 597 598 @override 599 bool get supportsHotReload => true; 600 601 @override 602 bool get supportsHotRestart => true; 603 604 @override 605 Future<bool> stopApp(ApplicationPackage app) { 606 final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]); 607 return runCommandAndStreamOutput(command).then<bool>( 608 (int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode)); 609 } 610 611 @override 612 void clearLogs() { 613 runSync(adbCommandForDevice(<String>['logcat', '-c'])); 614 } 615 616 @override 617 DeviceLogReader getLogReader({ ApplicationPackage app }) { 618 // The Android log reader isn't app-specific. 619 _logReader ??= _AdbLogReader(this); 620 return _logReader; 621 } 622 623 @override 624 DevicePortForwarder get portForwarder => _portForwarder ??= _AndroidDevicePortForwarder(this); 625 626 static final RegExp _timeRegExp = RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true); 627 628 /// Return the most recent timestamp in the Android log or [null] if there is 629 /// no available timestamp. The format can be passed to logcat's -T option. 630 String get lastLogcatTimestamp { 631 String output; 632 try { 633 output = runAdbCheckedSync(<String>[ 634 'shell', '-x', 'logcat', '-v', 'time', '-t', '1', 635 ]); 636 } catch (error) { 637 printError('Failed to extract the most recent timestamp from the Android log: $error.'); 638 return null; 639 } 640 final Match timeMatch = _timeRegExp.firstMatch(output); 641 return timeMatch?.group(0); 642 } 643 644 @override 645 bool isSupported() => true; 646 647 @override 648 bool get supportsScreenshot => true; 649 650 @override 651 Future<void> takeScreenshot(File outputFile) async { 652 const String remotePath = '/data/local/tmp/flutter_screenshot.png'; 653 await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]); 654 await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path])); 655 await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]); 656 } 657 658 @override 659 bool isSupportedForProject(FlutterProject flutterProject) { 660 return flutterProject.android.existsSync(); 661 } 662} 663 664Map<String, String> parseAdbDeviceProperties(String str) { 665 final Map<String, String> properties = <String, String>{}; 666 final RegExp propertyExp = RegExp(r'\[(.*?)\]: \[(.*?)\]'); 667 for (Match match in propertyExp.allMatches(str)) 668 properties[match.group(1)] = match.group(2); 669 return properties; 670} 671 672/// Return the list of connected ADB devices. 673List<AndroidDevice> getAdbDevices() { 674 final String adbPath = getAdbPath(androidSdk); 675 if (adbPath == null) 676 return <AndroidDevice>[]; 677 String text; 678 try { 679 text = runSync(<String>[adbPath, 'devices', '-l']); 680 } on ArgumentError catch (exception) { 681 throwToolExit('Unable to find "adb", check your Android SDK installation and ' 682 'ANDROID_HOME environment variable: ${exception.message}'); 683 } on ProcessException catch (exception) { 684 throwToolExit('Unable to run "adb", check your Android SDK installation and ' 685 'ANDROID_HOME environment variable: ${exception.executable}'); 686 } 687 final List<AndroidDevice> devices = <AndroidDevice>[]; 688 parseADBDeviceOutput(text, devices: devices); 689 return devices; 690} 691 692/// Get diagnostics about issues with any connected devices. 693Future<List<String>> getAdbDeviceDiagnostics() async { 694 final String adbPath = getAdbPath(androidSdk); 695 if (adbPath == null) 696 return <String>[]; 697 698 final RunResult result = await runAsync(<String>[adbPath, 'devices', '-l']); 699 if (result.exitCode != 0) { 700 return <String>[]; 701 } else { 702 final String text = result.stdout; 703 final List<String> diagnostics = <String>[]; 704 parseADBDeviceOutput(text, diagnostics: diagnostics); 705 return diagnostics; 706 } 707} 708 709// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper 710final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)'); 711 712/// Parse the given `adb devices` output in [text], and fill out the given list 713/// of devices and possible device issue diagnostics. Either argument can be null, 714/// in which case information for that parameter won't be populated. 715@visibleForTesting 716void parseADBDeviceOutput( 717 String text, { 718 List<AndroidDevice> devices, 719 List<String> diagnostics, 720}) { 721 // Check for error messages from adb 722 if (!text.contains('List of devices')) { 723 diagnostics?.add(text); 724 return; 725 } 726 727 for (String line in text.trim().split('\n')) { 728 // Skip lines like: * daemon started successfully * 729 if (line.startsWith('* daemon ')) 730 continue; 731 732 // Skip lines about adb server and client version not matching 733 if (line.startsWith(RegExp(r'adb server (version|is out of date)'))) { 734 diagnostics?.add(line); 735 continue; 736 } 737 738 if (line.startsWith('List of devices')) 739 continue; 740 741 if (_kDeviceRegex.hasMatch(line)) { 742 final Match match = _kDeviceRegex.firstMatch(line); 743 744 final String deviceID = match[1]; 745 final String deviceState = match[2]; 746 String rest = match[3]; 747 748 final Map<String, String> info = <String, String>{}; 749 if (rest != null && rest.isNotEmpty) { 750 rest = rest.trim(); 751 for (String data in rest.split(' ')) { 752 if (data.contains(':')) { 753 final List<String> fields = data.split(':'); 754 info[fields[0]] = fields[1]; 755 } 756 } 757 } 758 759 if (info['model'] != null) 760 info['model'] = cleanAdbDeviceName(info['model']); 761 762 if (deviceState == 'unauthorized') { 763 diagnostics?.add( 764 'Device $deviceID is not authorized.\n' 765 'You might need to check your device for an authorization dialog.' 766 ); 767 } else if (deviceState == 'offline') { 768 diagnostics?.add('Device $deviceID is offline.'); 769 } else { 770 devices?.add(AndroidDevice( 771 deviceID, 772 productID: info['product'], 773 modelID: info['model'] ?? deviceID, 774 deviceCodeName: info['device'], 775 )); 776 } 777 } else { 778 diagnostics?.add( 779 'Unexpected failure parsing device information from adb output:\n' 780 '$line\n' 781 'Please report a bug at https://github.com/flutter/flutter/issues/new/choose'); 782 } 783 } 784} 785 786/// A log reader that logs from `adb logcat`. 787class _AdbLogReader extends DeviceLogReader { 788 _AdbLogReader(this.device) { 789 _linesController = StreamController<String>.broadcast( 790 onListen: _start, 791 onCancel: _stop, 792 ); 793 } 794 795 final AndroidDevice device; 796 797 StreamController<String> _linesController; 798 Process _process; 799 800 @override 801 Stream<String> get logLines => _linesController.stream; 802 803 @override 804 String get name => device.name; 805 806 DateTime _timeOrigin; 807 808 DateTime _adbTimestampToDateTime(String adbTimestamp) { 809 // The adb timestamp format is: mm-dd hours:minutes:seconds.milliseconds 810 // Dart's DateTime parse function accepts this format so long as we provide 811 // the year, resulting in: 812 // yyyy-mm-dd hours:minutes:seconds.milliseconds. 813 return DateTime.parse('${DateTime.now().year}-$adbTimestamp'); 814 } 815 816 void _start() { 817 // Start the adb logcat process. 818 final List<String> args = <String>['shell', '-x', 'logcat', '-v', 'time']; 819 final String lastTimestamp = device.lastLogcatTimestamp; 820 if (lastTimestamp != null) 821 _timeOrigin = _adbTimestampToDateTime(lastTimestamp); 822 else 823 _timeOrigin = null; 824 runCommand(device.adbCommandForDevice(args)).then<void>((Process process) { 825 _process = process; 826 // We expect logcat streams to occasionally contain invalid utf-8, 827 // see: https://github.com/flutter/flutter/pull/8864. 828 const Utf8Decoder decoder = Utf8Decoder(reportErrors: false); 829 _process.stdout.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine); 830 _process.stderr.transform<String>(decoder).transform<String>(const LineSplitter()).listen(_onLine); 831 _process.exitCode.whenComplete(() { 832 if (_linesController.hasListener) 833 _linesController.close(); 834 }); 835 }); 836 } 837 838 // 'W/ActivityManager(pid): ' 839 static final RegExp _logFormat = RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s'); 840 841 static final List<RegExp> _whitelistedTags = <RegExp>[ 842 RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false), 843 RegExp(r'^[IE]\/DartVM[^:]*:\s+'), 844 RegExp(r'^[WEF]\/AndroidRuntime:\s+'), 845 RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'), 846 RegExp(r'^[WEF]\/System\.err:\s+'), 847 RegExp(r'^[F]\/[\S^:]+:\s+'), 848 ]; 849 850 // 'F/libc(pid): Fatal signal 11' 851 static final RegExp _fatalLog = RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)'); 852 853 // 'I/DEBUG(pid): ...' 854 static final RegExp _tombstoneLine = RegExp(r'^[IF]\/DEBUG\s*\(\s*\d+\):\s(.+)$'); 855 856 // 'I/DEBUG(pid): Tombstone written to: ' 857 static final RegExp _tombstoneTerminator = RegExp(r'^Tombstone written to:\s'); 858 859 // we default to true in case none of the log lines match 860 bool _acceptedLastLine = true; 861 862 // Whether a fatal crash is happening or not. 863 // During a fatal crash only lines from the crash are accepted, the rest are 864 // dropped. 865 bool _fatalCrash = false; 866 867 // The format of the line is controlled by the '-v' parameter passed to 868 // adb logcat. We are currently passing 'time', which has the format: 869 // mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): .... 870 void _onLine(String line) { 871 final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line); 872 if (timeMatch == null) { 873 return; 874 } 875 if (_timeOrigin != null) { 876 final String timestamp = timeMatch.group(0); 877 final DateTime time = _adbTimestampToDateTime(timestamp); 878 if (!time.isAfter(_timeOrigin)) { 879 // Ignore log messages before the origin. 880 return; 881 } 882 } 883 if (line.length == timeMatch.end) { 884 return; 885 } 886 // Chop off the time. 887 line = line.substring(timeMatch.end + 1); 888 final Match logMatch = _logFormat.firstMatch(line); 889 if (logMatch != null) { 890 bool acceptLine = false; 891 892 if (_fatalCrash) { 893 // While a fatal crash is going on, only accept lines from the crash 894 // Otherwise the crash log in the console may get interrupted 895 896 final Match fatalMatch = _tombstoneLine.firstMatch(line); 897 898 if (fatalMatch != null) { 899 acceptLine = true; 900 901 line = fatalMatch[1]; 902 903 if (_tombstoneTerminator.hasMatch(fatalMatch[1])) { 904 // Hit crash terminator, stop logging the crash info 905 _fatalCrash = false; 906 } 907 } 908 } else if (appPid != null && int.parse(logMatch.group(1)) == appPid) { 909 acceptLine = true; 910 911 if (_fatalLog.hasMatch(line)) { 912 // Hit fatal signal, app is now crashing 913 _fatalCrash = true; 914 } 915 } else { 916 // Filter on approved names and levels. 917 acceptLine = _whitelistedTags.any((RegExp re) => re.hasMatch(line)); 918 } 919 920 if (acceptLine) { 921 _acceptedLastLine = true; 922 _linesController.add(line); 923 return; 924 } 925 _acceptedLastLine = false; 926 } else if (line == '--------- beginning of system' || 927 line == '--------- beginning of main') { 928 // hide the ugly adb logcat log boundaries at the start 929 _acceptedLastLine = false; 930 } else { 931 // If it doesn't match the log pattern at all, then pass it through if we 932 // passed the last matching line through. It might be a multiline message. 933 if (_acceptedLastLine) { 934 _linesController.add(line); 935 return; 936 } 937 } 938 } 939 940 void _stop() { 941 // TODO(devoncarew): We should remove adb port forwarding here. 942 943 _process?.kill(); 944 } 945} 946 947class _AndroidDevicePortForwarder extends DevicePortForwarder { 948 _AndroidDevicePortForwarder(this.device); 949 950 final AndroidDevice device; 951 952 static int _extractPort(String portString) { 953 954 return int.tryParse(portString.trim()); 955 } 956 957 @override 958 List<ForwardedPort> get forwardedPorts { 959 final List<ForwardedPort> ports = <ForwardedPort>[]; 960 961 String stdout; 962 try { 963 stdout = runCheckedSync(device.adbCommandForDevice( 964 <String>['forward', '--list'] 965 )); 966 } catch (error) { 967 printError('Failed to list forwarded ports: $error.'); 968 return ports; 969 } 970 971 final List<String> lines = LineSplitter.split(stdout).toList(); 972 for (String line in lines) { 973 if (line.startsWith(device.id)) { 974 final List<String> splitLine = line.split('tcp:'); 975 976 // Sanity check splitLine. 977 if (splitLine.length != 3) 978 continue; 979 980 // Attempt to extract ports. 981 final int hostPort = _extractPort(splitLine[1]); 982 final int devicePort = _extractPort(splitLine[2]); 983 984 // Failed, skip. 985 if (hostPort == null || devicePort == null) 986 continue; 987 988 ports.add(ForwardedPort(hostPort, devicePort)); 989 } 990 } 991 992 return ports; 993 } 994 995 @override 996 Future<int> forward(int devicePort, { int hostPort }) async { 997 hostPort ??= 0; 998 final RunResult process = await runCheckedAsync(device.adbCommandForDevice( 999 <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort'] 1000 )); 1001 1002 if (process.stderr.isNotEmpty) 1003 process.throwException('adb returned error:\n${process.stderr}'); 1004 1005 if (process.exitCode != 0) { 1006 if (process.stdout.isNotEmpty) 1007 process.throwException('adb returned error:\n${process.stdout}'); 1008 process.throwException('adb failed without a message'); 1009 } 1010 1011 if (hostPort == 0) { 1012 if (process.stdout.isEmpty) 1013 process.throwException('adb did not report forwarded port'); 1014 hostPort = int.tryParse(process.stdout) ?? (throw 'adb returned invalid port number:\n${process.stdout}'); 1015 } else { 1016 // stdout may be empty or the port we asked it to forward, though it's 1017 // not documented (or obvious) what triggers each case. 1018 // 1019 // Observations are: 1020 // - On MacOS it's always empty when Flutter spawns the process, but 1021 // - On MacOS it prints the port number when run from the terminal, unless 1022 // the port is already forwarded, when it also prints nothing. 1023 // - On ChromeOS, the port appears to be printed even when Flutter spawns 1024 // the process 1025 // 1026 // To cover all cases, we accept the output being either empty or exactly 1027 // the port number, but treat any other output as probably being an error 1028 // message. 1029 if (process.stdout.isNotEmpty && process.stdout.trim() != '$hostPort') 1030 process.throwException('adb returned error:\n${process.stdout}'); 1031 } 1032 1033 return hostPort; 1034 } 1035 1036 @override 1037 Future<void> unforward(ForwardedPort forwardedPort) async { 1038 await runCheckedAsync(device.adbCommandForDevice( 1039 <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}'] 1040 )); 1041 } 1042} 1043