Commit e8222aaf authored by Zachary Anderson's avatar Zachary Anderson Committed by Flutter GitHub Bot

[flutter_tool] Don't crash on Android emulator startup failure (#48995)

parent 033c23f5
......@@ -8,8 +8,12 @@ import 'package:meta/meta.dart';
import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../device.dart';
import '../emulator.dart';
import '../globals.dart' as globals;
......@@ -50,18 +54,59 @@ class AndroidEmulator extends Emulator {
@override
Future<void> launch() async {
final Future<void> launchResult = processUtils.run(
<String>[getEmulatorPath(), '-avd', id],
throwOnError: true,
final Process process = await processUtils.start(
<String>[getEmulatorPath(androidSdk), '-avd', id],
);
// The emulator continues running on a successful launch, so if it hasn't
// quit within 3 seconds we assume that's a success and just return. This
// means that on a slow machine, a failure that takes more than three
// seconds won't be recognized as such... :-/
return Future.any<void>(<Future<void>>[
launchResult,
Future<void>.delayed(const Duration(seconds: 3)),
// Record output from the emulator process.
final List<String> stdoutList = <String>[];
final List<String> stderrList = <String>[];
final StreamSubscription<String> stdoutSubscription = process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(stdoutList.add);
final StreamSubscription<String> stderrSubscription = process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(stderrList.add);
final Future<void> stdioFuture = waitGroup<void>(<Future<void>>[
stdoutSubscription.asFuture<void>(),
stderrSubscription.asFuture<void>(),
]);
// The emulator continues running on success, so we don't wait for the
// process to complete before continuing. However, if the process fails
// after the startup phase (3 seconds), then we only echo its output if
// its error code is non-zero and its stderr is non-empty.
bool earlyFailure = true;
unawaited(process.exitCode.then((int status) async {
if (status == 0) {
globals.printTrace('The Android emulator exited successfully');
return;
}
// Make sure the process' stdout and stderr are drained.
await stdioFuture;
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
if (stdoutList.isNotEmpty) {
globals.printTrace('Android emulator stdout:');
stdoutList.forEach(globals.printTrace);
}
if (!earlyFailure && stderrList.isEmpty) {
globals.printStatus('The Android emulator exited with code $status');
return;
}
final String when = earlyFailure ? 'during startup' : 'after startup';
globals.printError('The Android emulator exited with code $status $when');
globals.printError('Android emulator stderr:');
stderrList.forEach(globals.printError);
globals.printError('Address these issues and try again.');
}));
// Wait a few seconds for the emulator to start.
await Future<void>.delayed(const Duration(seconds: 3));
earlyFailure = false;
return;
}
}
......
......@@ -69,17 +69,8 @@ class EmulatorsCommand extends FlutterCommand {
"More than one emulator matches '$id':",
);
} else {
try {
await emulators.first.launch();
}
catch (e) {
if (e is String) {
globals.printError(e);
} else {
rethrow;
}
}
}
}
Future<void> _createEmulator({ String name }) async {
......
......@@ -2,11 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'
show getEmulatorPath, AndroidSdk, androidSdk;
import 'package:flutter_tools/src/android/android_emulator.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/mocks.dart' show MockAndroidSdk;
void main() {
group('android_emulator', () {
......@@ -18,8 +29,10 @@ void main() {
});
testUsingContext('flags emulators with config', () {
const String emulatorID = '1234';
final AndroidEmulator emulator =
AndroidEmulator(emulatorID, <String, String>{'name': 'test'});
final AndroidEmulator emulator = AndroidEmulator(
emulatorID,
<String, String>{'name': 'test'},
);
expect(emulator.id, emulatorID);
expect(emulator.hasConfig, true);
});
......@@ -31,8 +44,7 @@ void main() {
'hw.device.manufacturer': manufacturer,
'avd.ini.displayname': displayName,
};
final AndroidEmulator emulator =
AndroidEmulator(emulatorID, properties);
final AndroidEmulator emulator = AndroidEmulator(emulatorID, properties);
expect(emulator.id, emulatorID);
expect(emulator.name, displayName);
expect(emulator.manufacturer, manufacturer);
......@@ -45,8 +57,7 @@ void main() {
final Map<String, String> properties = <String, String>{
'avd.ini.displayname': displayName,
};
final AndroidEmulator emulator =
AndroidEmulator(emulatorID, properties);
final AndroidEmulator emulator = AndroidEmulator(emulatorID, properties);
expect(emulator.name, displayName);
});
testUsingContext('uses cleaned up ID if no displayname is set', () {
......@@ -56,8 +67,7 @@ void main() {
final Map<String, String> properties = <String, String>{
'avd.ini.notadisplayname': 'this is not a display name',
};
final AndroidEmulator emulator =
AndroidEmulator(emulatorID, properties);
final AndroidEmulator emulator = AndroidEmulator(emulatorID, properties);
expect(emulator.name, 'This is my ID');
});
testUsingContext('parses ini files', () {
......@@ -74,4 +84,98 @@ void main() {
expect(results['avd.ini.displayname'], 'dispName');
});
});
group('Android emulator launch ', () {
const String emulatorID = 'i1234';
const String errorText = '[Android emulator test error]';
MockAndroidSdk mockSdk;
FakeProcessManager successProcessManager;
FakeProcessManager errorProcessManager;
FakeProcessManager lateFailureProcessManager;
MemoryFileSystem fs;
setUp(() {
fs = MemoryFileSystem();
mockSdk = MockAndroidSdk();
when(mockSdk.emulatorPath).thenReturn('emulator');
const List<String> command = <String>[
'emulator', '-avd', emulatorID,
];
successProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: command),
]);
errorProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: command,
exitCode: 1,
stderr: errorText,
stdout: 'dummy text',
duration: Duration(seconds: 1),
),
]);
lateFailureProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: command,
exitCode: 1,
stderr: '',
stdout: 'dummy text',
duration: Duration(seconds: 4),
),
]);
});
testUsingContext('succeeds', () async {
final AndroidEmulator emulator = AndroidEmulator(emulatorID);
expect(getEmulatorPath(androidSdk), mockSdk.emulatorPath);
final Completer<void> completer = Completer<void>();
FakeAsync().run((FakeAsync time) {
unawaited(emulator.launch().whenComplete(completer.complete));
time.elapse(const Duration(seconds: 5));
time.flushMicrotasks();
});
await completer.future;
}, overrides: <Type, Generator>{
ProcessManager: () => successProcessManager,
AndroidSdk: () => mockSdk,
FileSystem: () => fs,
});
testUsingContext('prints error on failure', () async {
final AndroidEmulator emulator = AndroidEmulator(emulatorID);
final Completer<void> completer = Completer<void>();
FakeAsync().run((FakeAsync time) {
unawaited(emulator.launch().whenComplete(completer.complete));
time.elapse(const Duration(seconds: 5));
time.flushMicrotasks();
});
await completer.future;
expect(testLogger.errorText, contains(errorText));
}, overrides: <Type, Generator>{
ProcessManager: () => errorProcessManager,
AndroidSdk: () => mockSdk,
FileSystem: () => fs,
});
testUsingContext('prints nothing on late failure with empty stderr', () async {
final AndroidEmulator emulator = AndroidEmulator(emulatorID);
final Completer<void> completer = Completer<void>();
FakeAsync().run((FakeAsync time) async {
unawaited(emulator.launch().whenComplete(completer.complete));
time.elapse(const Duration(seconds: 5));
time.flushMicrotasks();
});
await completer.future;
expect(testLogger.errorText, isEmpty);
}, overrides: <Type, Generator>{
ProcessManager: () => lateFailureProcessManager,
AndroidSdk: () => mockSdk,
FileSystem: () => fs,
});
});
}
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