Add runWithAvd method to AvdManager
This is the first step towards removing
android-device-provider-gradle UTP plugin by
consolidating AVD launcher implementation in
AGP.
Bug: 431284686
Test: AvdSnapshotHandlerTest
Change-Id: I641763297f2fc0c42bb8f3637a6c4b2002e6fd30
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt
index 81297a2..17bb9a8 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdComponents.kt
@@ -174,10 +174,30 @@
avdManager.get().loadSnapshotIfNeeded(deviceName, emulatorGpuMode)
}
+ /**
+ * Manages and runs an Android Virtual Device (AVD) for a given operation.
+ *
+ * This function starts the specified AVD, waits for it to come online, and then executes the
+ * [onDeviceReady] callback with the device's serial number. The function is blocking and will
+ * not return until the callback has completed.
+ *
+ * The [onDeviceReady] callback is invoked on the same thread that called this method.
+ *
+ * @param deviceName The name of the AVD to provision. This AVD must have been created beforehand.
+ * @param emulatorGpuMode The GPU mode to use when starting the emulator.
+ * @param onDeviceReady A block of code to execute once the device is ready. It is provided with the
+ * online device's serial number.
+ * @throws RuntimeException if the device cannot be provisioned or fails to start.
+ */
+ fun runWithAvd(deviceName: String, emulatorGpuMode: String,
+ onDeviceReady: (onlineDeviceSerial: String) -> Unit) {
+ avdManager.get().runWithAvd(deviceName, emulatorGpuMode, onDeviceReady)
+ }
+
/** Closes all active emulators having an id with the given prefix. This should be used to close
* emulators that may remain after a crashed UTP test run.
*
- * @param idPrefix the prefix that is looke for to close the active emulators. All emulators
+ * @param idPrefix the prefix that is looked for to close the active emulators. All emulators
* that have an id not starting with this prefix are ignored.
*/
fun closeOpenEmulators(idPrefix: String) {
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt
index 88d681b..eb22833 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdManager.kt
@@ -231,6 +231,28 @@
}
/**
+ * Starts an Android Virtual device and [onDeviceReady] callback is invoked once the device
+ * becomes online and system services on the device are started.
+ *
+ * This method blocks execution and [onDeviceReady] callback is invoked from the caller's thread.
+ */
+ fun runWithAvd(deviceName: String, emulatorGpuFlag: String,
+ onDeviceReady: (onlineDeviceSerial: String) -> Unit) {
+ runWithMultiProcessLocking(deviceName) {
+ deviceLockManager.lock(1).use {
+ snapshotHandler.startEmulatorThenStop(
+ false,
+ deviceName,
+ avdFolder,
+ emulatorGpuFlag,
+ logger,
+ onDeviceReady
+ )
+ }
+ }
+ }
+
+ /**
* Returns the names of all avds currently in the shared avd folder.
*/
fun allAvds(): List<String> {
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt
index 404db81..da7cfe2 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/AvdSnapshotHandler.kt
@@ -16,24 +16,23 @@
package com.android.build.gradle.internal
-import com.android.build.gradle.internal.testing.getEmulatorMetadata
import com.android.build.gradle.internal.testing.AdbHelper
import com.android.build.gradle.internal.testing.EmulatorVersionMetadata
import com.android.build.gradle.internal.testing.QemuExecutor
+import com.android.build.gradle.internal.testing.getEmulatorMetadata
import com.android.sdklib.internal.avd.AvdManager
import com.android.testing.utils.createSetupDeviceId
-import com.android.utils.FileUtils
import com.android.utils.GrabProcessOutput
import com.android.utils.ILogger
-import java.io.File
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.Executor
-import java.util.concurrent.Executors
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
+import java.io.File
import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
private const val EMULATOR_EXECUTABLE = "emulator"
private const val DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC = 600L
@@ -55,7 +54,7 @@
private val emulatorDir: Provider<Directory>,
private val qemuExecutor: QemuExecutor,
private val extraWaitAfterBootCompleteMs: Long = WAIT_AFTER_BOOT_MS,
- private val executor: Executor = Executors.newSingleThreadExecutor(),
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor(),
private val metadataFactory: (File) -> EmulatorVersionMetadata = ::getEmulatorMetadata,
private val processFactory: (List<String>) -> ProcessBuilder = { ProcessBuilder(it) }) {
@@ -65,6 +64,9 @@
metadataFactory(emulatorDirectory)
}
+ private val timeoutSeconds: Long
+ get() = deviceBootAndSnapshotCheckTimeoutSec ?: DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
+
/**
* Checks whether the emulator directory contains a valid emulator executable, and returns it.
*
@@ -173,21 +175,15 @@
logger.warning("Timed out trying to check $snapshotName for $avdName is loadable.")
}
if (!timeout) {
- val timeoutSec =
- deviceBootAndSnapshotCheckTimeoutSec ?:
- DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
- outputProcessed.await(timeoutSec, TimeUnit.SECONDS)
+ outputProcessed.await(timeoutSeconds, TimeUnit.SECONDS)
}
return success.get()
}
private fun Process.waitUntilTimeout(logger: ILogger, onTimeout: () -> Unit) {
- val timeoutSec =
- deviceBootAndSnapshotCheckTimeoutSec ?:
- DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
- if (timeoutSec > 0) {
- logger.verbose("Waiting for a process to complete (timeout $timeoutSec seconds)")
- if (!waitFor(timeoutSec, TimeUnit.SECONDS)) {
+ if (timeoutSeconds > 0) {
+ logger.verbose("Waiting for a process to complete (timeout $timeoutSeconds seconds)")
+ if (!waitFor(timeoutSeconds, TimeUnit.SECONDS)) {
onTimeout()
}
} else {
@@ -293,12 +289,19 @@
}
}
- private fun startEmulatorThenStop(
+ /**
+ * Starts an Android Virtual device and [onDeviceReady] callback is invoked once the device
+ * becomes online and system services on the device are started.
+ *
+ * This method blocks execution and [onDeviceReady] callback is invoked from the caller's thread.
+ */
+ fun startEmulatorThenStop(
createSnapshot: Boolean,
avdName: String,
avdLocation: File,
emulatorGpuFlag: String,
- logger: ILogger
+ logger: ILogger,
+ onDeviceReady: (onlineDeviceSerial: String) -> Unit = {},
) {
val deviceId = createSetupDeviceId(avdName)
@@ -316,17 +319,14 @@
)
)
processBuilder.environment()["ANDROID_AVD_HOME"] = avdLocation.absolutePath
- processBuilder.environment()["ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL"] = (
- deviceBootAndSnapshotCheckTimeoutSec ?:
- DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
- ).toString()
+ processBuilder.environment()["ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL"] = timeoutSeconds.toString()
val emulatorProcess = processBuilder.start()
val bootCompleted = AtomicBoolean(false)
// need to process both stderr and stdout
val outputProcessed = CountDownLatch(2)
val emulatorErrorList = mutableListOf<String>()
try {
- executor.execute {
+ val deviceSerialFuture = executor.submit<String> {
var emulatorSerial: String? = null
while(emulatorProcess.isAlive) {
try {
@@ -341,7 +341,7 @@
if (emulatorSerial == null) {
// It is possible for the emulator process to return unexpectedly
// and the emulatorSerial to not be set.
- return@execute
+ return@submit null
}
logger.verbose("$avdName is attached to adb ($emulatorSerial).")
@@ -372,11 +372,12 @@
if (emulatorProcess.isAlive) {
logger.verbose("$avdName is ready to take a snapshot.")
bootCompleted.set(true)
- adbHelper.killDevice(emulatorSerial)
+ return@submit emulatorSerial
} else {
logger.warning(
"Emulator process exited unexpectedly with the return code " +
"${emulatorProcess.exitValue()}.")
+ return@submit null
}
}
@@ -403,6 +404,12 @@
}
)
+ val deviceSerial = deviceSerialFuture.get(timeoutSeconds, TimeUnit.SECONDS)
+ if (deviceSerial != null) {
+ onDeviceReady(deviceSerial)
+ adbHelper.killDevice(deviceSerial)
+ }
+
emulatorProcess.waitUntilTimeout(logger) {
logger.warning("Snapshot creation timed out. Closing emulator.")
throw EmulatorSnapshotCannotCreatedException("""
@@ -416,15 +423,9 @@
emulatorProcess.destroy()
}
-
-
if (!bootCompleted.get()) {
// wait for processing to complete for output of process
-
- val timeoutSec =
- deviceBootAndSnapshotCheckTimeoutSec ?:
- DEFAULT_DEVICE_BOOT_AND_SNAPSHOT_CHECK_TIMEOUT_SEC
- outputProcessed.await(timeoutSec, TimeUnit.SECONDS)
+ outputProcessed.await(timeoutSeconds, TimeUnit.SECONDS)
throw EmulatorSnapshotCannotCreatedException(
"""
diff --git a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt
index 40de670..9c106dd 100644
--- a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt
+++ b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/AvdSnapshotHandlerTest.kt
@@ -22,10 +22,6 @@
import com.android.build.gradle.internal.testing.QemuExecutor
import com.android.sdklib.internal.avd.AvdInfo
import com.android.sdklib.internal.avd.AvdManager
-import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.mock
-import com.android.utils.FileUtils
import com.android.utils.ILogger
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors
@@ -38,15 +34,14 @@
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.kotlin.any
import org.mockito.Mockito.contains
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-import org.mockito.junit.MockitoJUnit
import java.io.File
import java.io.InputStream
-import java.lang.RuntimeException
@RunWith(JUnit4::class)
class AvdSnapshotHandlerTest {
@@ -109,7 +104,7 @@
emulatorDirectoryProvider,
qemuExecutor,
extraWaitAfterBootCompleteMs = 0L,
- MoreExecutors.directExecutor(),
+ MoreExecutors.newDirectExecutorService(),
{ _ -> EmulatorVersionMetadata(true) }
) { commands ->
if (commands.contains("-check-snapshot-loadable")) {
@@ -138,7 +133,7 @@
emulatorDirectoryProvider,
qemuExecutor,
extraWaitAfterBootCompleteMs = 0L,
- MoreExecutors.directExecutor(),
+ MoreExecutors.newDirectExecutorService(),
{ _ -> EmulatorVersionMetadata(true) }
) { _ -> createMockProcessBuilder() }
@@ -182,7 +177,7 @@
emulatorDirectoryProvider,
qemuExecutor,
extraWaitAfterBootCompleteMs = 0L,
- MoreExecutors.directExecutor(),
+ MoreExecutors.newDirectExecutorService(),
{ _ -> EmulatorVersionMetadata(true) }
) { commands ->
assertThat(commands).contains("-verbose")
@@ -222,7 +217,7 @@
emulatorDirectoryProvider,
qemuExecutor,
extraWaitAfterBootCompleteMs = 0L,
- MoreExecutors.directExecutor(),
+ MoreExecutors.newDirectExecutorService(),
{ _ -> EmulatorVersionMetadata(true) }
) { commands ->
// disabling full kernel logging should disable verbose logging in setup actions.
@@ -256,4 +251,31 @@
emulatorError
)
}
+
+ @Test
+ fun startEmulatorThenStop() {
+ val env = mutableMapOf<String, String>()
+ val handler = AvdSnapshotHandler(
+ showFullEmulatorKernelLogging = true,
+ deviceBootAndSnapshotCheckTimeoutSec = 1234,
+ mockAdbHelper,
+ emulatorDirectoryProvider,
+ qemuExecutor,
+ extraWaitAfterBootCompleteMs = 0L,
+ MoreExecutors.newDirectExecutorService(),
+ { _ -> EmulatorVersionMetadata(true) }
+ ) { _ -> createMockProcessBuilder() }
+
+ var onDeviceReadyIsCalled = false
+ handler.startEmulatorThenStop(
+ createSnapshot = false,
+ "myTestAvdName",
+ avdDirectory,
+ emulatorGpuFlag = "",
+ mockLogger) {
+ onDeviceReadyIsCalled = true
+ }
+
+ assertThat(onDeviceReadyIsCalled).isTrue()
+ }
}