Unverified Commit 30dbc4c7 authored by Alexander Aprelev's avatar Alexander Aprelev Committed by GitHub

Whitelist adb.exe heap corruption exit code. (#33951)

* Whitelist adb.exe heap corruption exit code.

In android platform tools 29.0.0 adb.exe shell seems to be exiting with heap corruption exit code, otherwise producing results as expected. This PR whitelists this exit code on Windows.

Fixes https://github.com/flutter/flutter/issues/33938.

* Fix condition

* Fix 'shell am start' command

* Fix stop command

* Refactor into runAdbMostlyChecked(Sync/Async)

* runAdbMostlyChecked -> runAdbChecked
parent 5555725f
...@@ -14,6 +14,7 @@ import '../base/common.dart' show throwToolExit; ...@@ -14,6 +14,7 @@ import '../base/common.dart' show throwToolExit;
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/process_manager.dart'; import '../base/process_manager.dart';
import '../build_info.dart'; import '../build_info.dart';
...@@ -42,6 +43,13 @@ const Map<String, _HardwareType> _knownHardware = <String, _HardwareType>{ ...@@ -42,6 +43,13 @@ const Map<String, _HardwareType> _knownHardware = <String, _HardwareType>{
'samsungexynos9810': _HardwareType.physical, 'samsungexynos9810': _HardwareType.physical,
}; };
bool allowHeapCorruptionOnWindows(int exitCode) {
// In platform tools 29.0.0 adb.exe seems to be ending with this heap
// corruption error code on seemingly successful termination.
// So we ignore this error on Windows.
return exitCode == -1073740940 && platform.isWindows;
}
class AndroidDevices extends PollingDeviceDiscovery { class AndroidDevices extends PollingDeviceDiscovery {
AndroidDevices() : super('Android devices'); AndroidDevices() : super('Android devices');
...@@ -89,10 +97,10 @@ class AndroidDevice extends Device { ...@@ -89,10 +97,10 @@ class AndroidDevice extends Device {
stdoutEncoding: latin1, stdoutEncoding: latin1,
stderrEncoding: latin1, stderrEncoding: latin1,
); );
if (result.exitCode == 0) { if (result.exitCode == 0 || allowHeapCorruptionOnWindows(result.exitCode)) {
_properties = parseAdbDeviceProperties(result.stdout); _properties = parseAdbDeviceProperties(result.stdout);
} else { } else {
printError('Error retrieving device properties for $name:'); printError('Error ${result.exitCode} retrieving device properties for $name:');
printError(result.stderr); printError(result.stderr);
} }
} on ProcessException catch (error) { } on ProcessException catch (error) {
...@@ -159,6 +167,28 @@ class AndroidDevice extends Device { ...@@ -159,6 +167,28 @@ class AndroidDevice extends Device {
return <String>[getAdbPath(androidSdk), '-s', id]..addAll(args); return <String>[getAdbPath(androidSdk), '-s', id]..addAll(args);
} }
String runAdbCheckedSync(
List<String> params, {
String workingDirectory,
bool allowReentrantFlutter = false,
Map<String, String> environment}) {
return runCheckedSync(adbCommandForDevice(params), workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment,
whiteListFailures: allowHeapCorruptionOnWindows
);
}
Future<RunResult> runAdbCheckedAsync(
List<String> params, {
String workingDirectory,
bool allowReentrantFlutter = false,
}) async {
return runCheckedAsync(adbCommandForDevice(params), workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
whiteListFailures: allowHeapCorruptionOnWindows);
}
bool _isValidAdbVersion(String adbVersion) { bool _isValidAdbVersion(String adbVersion) {
// Sample output: 'Android Debug Bridge version 1.0.31' // Sample output: 'Android Debug Bridge version 1.0.31'
final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion); final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
...@@ -252,7 +282,7 @@ class AndroidDevice extends Device { ...@@ -252,7 +282,7 @@ class AndroidDevice extends Device {
Future<bool> isAppInstalled(ApplicationPackage app) async { Future<bool> isAppInstalled(ApplicationPackage app) async {
// This call takes 400ms - 600ms. // This call takes 400ms - 600ms.
try { try {
final RunResult listOut = await runCheckedAsync(adbCommandForDevice(<String>['shell', 'pm', 'list', 'packages', app.id])); final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]);
return LineSplitter.split(listOut.stdout).contains('package:${app.id}'); return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
} catch (error) { } catch (error) {
printTrace('$error'); printTrace('$error');
...@@ -294,9 +324,9 @@ class AndroidDevice extends Device { ...@@ -294,9 +324,9 @@ class AndroidDevice extends Device {
return false; return false;
} }
await runCheckedAsync(adbCommandForDevice(<String>[ await runAdbCheckedAsync(<String>[
'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app), 'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
])); ]);
return true; return true;
} }
...@@ -413,13 +443,13 @@ class AndroidDevice extends Device { ...@@ -413,13 +443,13 @@ class AndroidDevice extends Device {
List<String> cmd; List<String> cmd;
cmd = adbCommandForDevice(<String>[ cmd = <String>[
'shell', 'am', 'start', 'shell', 'am', 'start',
'-a', 'android.intent.action.RUN', '-a', 'android.intent.action.RUN',
'-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
'--ez', 'enable-background-compilation', 'true', '--ez', 'enable-background-compilation', 'true',
'--ez', 'enable-dart-profiling', 'true', '--ez', 'enable-dart-profiling', 'true',
]); ];
if (traceStartup) if (traceStartup)
cmd.addAll(<String>['--ez', 'trace-startup', 'true']); cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
...@@ -451,7 +481,7 @@ class AndroidDevice extends Device { ...@@ -451,7 +481,7 @@ class AndroidDevice extends Device {
} }
} }
cmd.add(apk.launchActivity); cmd.add(apk.launchActivity);
final String result = (await runCheckedAsync(cmd)).stdout; final String result = (await runAdbCheckedAsync(cmd)).stdout;
// This invocation returns 0 even when it fails. // This invocation returns 0 even when it fails.
if (result.contains('Error: ')) { if (result.contains('Error: ')) {
printError(result.trim(), wrap: false); printError(result.trim(), wrap: false);
...@@ -491,7 +521,8 @@ class AndroidDevice extends Device { ...@@ -491,7 +521,8 @@ class AndroidDevice extends Device {
@override @override
Future<bool> stopApp(ApplicationPackage app) { Future<bool> stopApp(ApplicationPackage app) {
final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]); final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
return runCommandAndStreamOutput(command).then<bool>((int exitCode) => exitCode == 0); return runCommandAndStreamOutput(command).then<bool>(
(int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode));
} }
@override @override
...@@ -514,9 +545,9 @@ class AndroidDevice extends Device { ...@@ -514,9 +545,9 @@ class AndroidDevice extends Device {
/// Return the most recent timestamp in the Android log or null if there is /// Return the most recent timestamp in the Android log or null if there is
/// no available timestamp. The format can be passed to logcat's -T option. /// no available timestamp. The format can be passed to logcat's -T option.
String get lastLogcatTimestamp { String get lastLogcatTimestamp {
final String output = runCheckedSync(adbCommandForDevice(<String>[ final String output = runAdbCheckedSync(<String>[
'shell', '-x', 'logcat', '-v', 'time', '-t', '1', 'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
])); ]);
final Match timeMatch = _timeRegExp.firstMatch(output); final Match timeMatch = _timeRegExp.firstMatch(output);
return timeMatch?.group(0); return timeMatch?.group(0);
...@@ -531,9 +562,9 @@ class AndroidDevice extends Device { ...@@ -531,9 +562,9 @@ class AndroidDevice extends Device {
@override @override
Future<void> takeScreenshot(File outputFile) async { Future<void> takeScreenshot(File outputFile) async {
const String remotePath = '/data/local/tmp/flutter_screenshot.png'; const String remotePath = '/data/local/tmp/flutter_screenshot.png';
await runCheckedAsync(adbCommandForDevice(<String>['shell', 'screencap', '-p', remotePath])); await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path])); await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
await runCheckedAsync(adbCommandForDevice(<String>['shell', 'rm', remotePath])); await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]);
} }
@override @override
......
...@@ -239,11 +239,14 @@ Future<RunResult> runAsync( ...@@ -239,11 +239,14 @@ Future<RunResult> runAsync(
return runResults; return runResults;
} }
typedef RunResultChecker = bool Function(int);
Future<RunResult> runCheckedAsync( Future<RunResult> runCheckedAsync(
List<String> cmd, { List<String> cmd, {
String workingDirectory, String workingDirectory,
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
Map<String, String> environment, Map<String, String> environment,
RunResultChecker whiteListFailures,
}) async { }) async {
final RunResult result = await runAsync( final RunResult result = await runAsync(
cmd, cmd,
...@@ -252,8 +255,10 @@ Future<RunResult> runCheckedAsync( ...@@ -252,8 +255,10 @@ Future<RunResult> runCheckedAsync(
environment: environment, environment: environment,
); );
if (result.exitCode != 0) { if (result.exitCode != 0) {
throw ProcessException(cmd[0], cmd.sublist(1), if (whiteListFailures == null || !whiteListFailures(result.exitCode)) {
'Process "${cmd[0]}" exited abnormally:\n$result', result.exitCode); throw ProcessException(cmd[0], cmd.sublist(1),
'Process "${cmd[0]}" exited abnormally:\n$result', result.exitCode);
}
} }
return result; return result;
} }
...@@ -287,6 +292,7 @@ String runCheckedSync( ...@@ -287,6 +292,7 @@ String runCheckedSync(
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
bool hideStdout = false, bool hideStdout = false,
Map<String, String> environment, Map<String, String> environment,
RunResultChecker whiteListFailures,
}) { }) {
return _runWithLoggingSync( return _runWithLoggingSync(
cmd, cmd,
...@@ -296,6 +302,7 @@ String runCheckedSync( ...@@ -296,6 +302,7 @@ String runCheckedSync(
checked: true, checked: true,
noisyErrors: true, noisyErrors: true,
environment: environment, environment: environment,
whiteListFailures: whiteListFailures
); );
} }
...@@ -330,6 +337,7 @@ String _runWithLoggingSync( ...@@ -330,6 +337,7 @@ String _runWithLoggingSync(
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
bool hideStdout = false, bool hideStdout = false,
Map<String, String> environment, Map<String, String> environment,
RunResultChecker whiteListFailures,
}) { }) {
_traceCommand(cmd, workingDirectory: workingDirectory); _traceCommand(cmd, workingDirectory: workingDirectory);
final ProcessResult results = processManager.runSync( final ProcessResult results = processManager.runSync(
...@@ -340,14 +348,19 @@ String _runWithLoggingSync( ...@@ -340,14 +348,19 @@ String _runWithLoggingSync(
printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}'); printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}');
bool failedExitCode = results.exitCode != 0;
if (whiteListFailures != null && failedExitCode) {
failedExitCode = !whiteListFailures(results.exitCode);
}
if (results.stdout.isNotEmpty && !hideStdout) { if (results.stdout.isNotEmpty && !hideStdout) {
if (results.exitCode != 0 && noisyErrors) if (failedExitCode && noisyErrors)
printStatus(results.stdout.trim()); printStatus(results.stdout.trim());
else else
printTrace(results.stdout.trim()); printTrace(results.stdout.trim());
} }
if (results.exitCode != 0) { if (failedExitCode) {
if (results.stderr.isNotEmpty) { if (results.stderr.isNotEmpty) {
if (noisyErrors) if (noisyErrors)
printError(results.stderr.trim()); printError(results.stderr.trim());
......
...@@ -10,6 +10,7 @@ import 'package:flutter_tools/src/android/android_sdk.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -106,6 +107,84 @@ Use the 'android' tool to install them: ...@@ -106,6 +107,84 @@ Use the 'android' tool to install them:
}); });
}); });
group('adb.exe exiting with heap corruption on windows', () {
final ProcessManager mockProcessManager = MockProcessManager();
String hardware;
String buildCharacteristics;
setUp(() {
hardware = 'goldfish';
buildCharacteristics = 'unused';
exitCode = -1;
when(mockProcessManager.run(argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'))).thenAnswer((_) {
final StringBuffer buf = StringBuffer()
..writeln('[ro.hardware]: [$hardware]')..writeln(
'[ro.build.characteristics]: [$buildCharacteristics]');
final ProcessResult result = ProcessResult(1, exitCode, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
});
testUsingContext('nonHeapCorruptionErrorOnWindows', () async {
exitCode = -1073740941;
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => FakePlatform(
operatingSystem: 'windows',
environment: <String, String>{
'ANDROID_HOME': '/',
},
),
});
testUsingContext('heapCorruptionOnWindows', () async {
exitCode = -1073740940;
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, true);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => FakePlatform(
operatingSystem: 'windows',
environment: <String, String>{
'ANDROID_HOME': '/',
},
),
});
testUsingContext('heapCorruptionExitCodeOnLinux', () async {
exitCode = -1073740940;
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{
'ANDROID_HOME': '/',
},
),
});
testUsingContext('noErrorOnLinux', () async {
exitCode = 0;
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, true);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{
'ANDROID_HOME': '/',
},
),
});
});
group('isLocalEmulator', () { group('isLocalEmulator', () {
final ProcessManager mockProcessManager = MockProcessManager(); final ProcessManager mockProcessManager = MockProcessManager();
String hardware; String hardware;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment