diff --git a/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/AndroidJitEmulatorDetector.java b/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/AndroidJitEmulatorDetector.java new file mode 100644 index 000000000..daa6972bd --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/AndroidJitEmulatorDetector.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.api.devicemanager.detector; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.AndroidEmulatorType; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.DetectionResult; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.DetectionResult.DetectionType; +import com.google.devtools.mobileharness.shared.util.flags.Flags; +import javax.annotation.Nullable; + +/** Detector for Android JIT emulators. */ +public final class AndroidJitEmulatorDetector implements Detector { + /** Initialized in {@link #precondition()}. */ + @Nullable private volatile ImmutableList emulatorIds; + + // Acloud creates cuttlefish emulator with port 6520 + index, where index is instance id minus + // one. Example: "acloud create --local-instance 1" will create a local emulator with port 6520. + // And instance 2 will be 6521, so on and so forth. + private static final int EMULATOR_BASE_PORT = 6520; + + public AndroidJitEmulatorDetector() {} + + /** + * Precondition of the detector. + * + * @return whether the precondition meets + */ + @Override + public boolean precondition() { + int emulatorNumber = Flags.instance().androidJitEmulatorNum.getNonNull(); + ImmutableList.Builder emulatorIdsBuilder = ImmutableList.builder(); + for (int i = 0; i < emulatorNumber; i++) { + emulatorIdsBuilder.add("0.0.0.0:" + (i + EMULATOR_BASE_PORT)); + } + emulatorIds = emulatorIdsBuilder.build(); + return emulatorNumber > 0; + } + + /** + * Detects the Android JIT emulators. + * + * @return the detection result of the Android JIT emulators + */ + @Override + public ImmutableList detectDevices() { + return emulatorIds.stream() + .map(id -> DetectionResult.of(id, DetectionType.EMULATOR, AndroidEmulatorType.JIT_EMULATOR)) + .collect(toImmutableList()); + } +} diff --git a/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/BUILD b/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/BUILD index ac47666d7..907a5a9e8 100644 --- a/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/BUILD +++ b/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/BUILD @@ -23,6 +23,19 @@ package( default_visibility = ["//:deviceinfra_devicemanagement_pkg"], ) +java_library( + name = "android_jit_emulator_detector", + srcs = ["AndroidJitEmulatorDetector.java"], + deps = [ + ":base", + "//src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model", + "//src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model:android_emulator_type", + "//src/java/com/google/devtools/mobileharness/shared/util/flags", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "base", srcs = ["Detector.java"], diff --git a/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model/AndroidEmulatorType.java b/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model/AndroidEmulatorType.java index e454a6b33..c3f4dd5e3 100644 --- a/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model/AndroidEmulatorType.java +++ b/src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model/AndroidEmulatorType.java @@ -17,4 +17,6 @@ package com.google.devtools.mobileharness.api.devicemanager.detector.model; /** Android emulator device types */ -public enum AndroidEmulatorType {} +public enum AndroidEmulatorType { + JIT_EMULATOR, +} diff --git a/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/AndroidJitEmulatorDispatcher.java b/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/AndroidJitEmulatorDispatcher.java new file mode 100644 index 000000000..7ae85ffb9 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/AndroidJitEmulatorDispatcher.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.api.devicemanager.dispatcher; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.AndroidEmulatorType; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.DetectionResult; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.DetectionResult.DetectionType; +import com.google.devtools.mobileharness.api.devicemanager.detector.model.DetectionResults; +import com.google.devtools.mobileharness.api.devicemanager.dispatcher.model.DispatchResult.DispatchType; +import com.google.devtools.mobileharness.api.devicemanager.dispatcher.model.DispatchResults; +import com.google.devtools.mobileharness.api.devicemanager.dispatcher.util.DeviceIdGenerator; +import com.google.devtools.mobileharness.api.model.lab.DeviceId; + +/** Dispatcher for Android JIT Emulator devices. */ +public final class AndroidJitEmulatorDispatcher implements Dispatcher { + private final DeviceIdGenerator deviceIdGenerator; + + public AndroidJitEmulatorDispatcher() { + this(new DeviceIdGenerator()); + } + + @VisibleForTesting + AndroidJitEmulatorDispatcher(DeviceIdGenerator deviceIdGenerator) { + this.deviceIdGenerator = deviceIdGenerator; + } + + @Override + public ImmutableMap dispatchDevices( + DetectionResults detectionResults, DispatchResults dispatchResults) { + return detectionResults + .getByTypeAndDetail(DetectionType.EMULATOR, AndroidEmulatorType.JIT_EMULATOR) + .stream() + .collect(toImmutableMap(DetectionResult::deviceControlId, s -> DispatchType.LIVE)); + } + + @Override + public DeviceId generateDeviceId(String deviceControlId) { + return deviceIdGenerator.getAndroidDeviceId(deviceControlId); + } +} diff --git a/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/BUILD b/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/BUILD index e6ea2c55d..e51bfed67 100644 --- a/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/BUILD +++ b/src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/BUILD @@ -70,6 +70,20 @@ java_library( ], ) +java_library( + name = "android_jit_emulator_dispatcher", + srcs = ["AndroidJitEmulatorDispatcher.java"], + deps = [ + ":base", + "//src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model", + "//src/java/com/google/devtools/mobileharness/api/devicemanager/detector/model:android_emulator_type", + "//src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/model", + "//src/java/com/google/devtools/mobileharness/api/devicemanager/dispatcher/util:device_id_descriptor_generator", + "//src/java/com/google/devtools/mobileharness/api/model/lab:device_id_descriptor", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "android_real_device", srcs = ["AndroidRealDeviceDispatcher.java"], diff --git a/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java b/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java index e1867469e..c36d35a5d 100644 --- a/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java +++ b/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java @@ -47,6 +47,14 @@ public class Flags { converter = Flag.StringConverter.class) public Flag aaptPath = aaptPathDefault; + private static final Flag acloudPathDefault = Flag.value("/bin/acloud_prebuilt"); + + @com.beust.jcommander.Parameter( + names = "--acloud_path", + description = "Path to the acloud binary.", + converter = Flag.StringConverter.class) + public Flag acloudPath = acloudPathDefault; + private static final Flag adbCommandRetryAttemptsDefault = Flag.value(2); @com.beust.jcommander.Parameter( @@ -216,6 +224,16 @@ public class Flags { converter = DurationFlag.DurationConverter.class) public Flag androidFactoryResetWaitTime = androidFactoryResetWaitTimeDefault; + private static final Flag androidJitEmulatorNumDefault = Flag.value(0); + + @com.beust.jcommander.Parameter( + names = "--android_jit_emulator_num", + description = + "The naximum number of android Just-in-time emulators that could be run on the server" + + " simultaneously.", + converter = Flag.IntegerConverter.class) + public Flag androidJitEmulatorNum = androidJitEmulatorNumDefault; + private static final Flag apiConfigFileDefault = Flag.value(""); @com.beust.jcommander.Parameter( diff --git a/src/java/com/google/wireless/qa/mobileharness/shared/api/device/AndroidJitEmulator.java b/src/java/com/google/wireless/qa/mobileharness/shared/api/device/AndroidJitEmulator.java new file mode 100644 index 000000000..ec8af9dba --- /dev/null +++ b/src/java/com/google/wireless/qa/mobileharness/shared/api/device/AndroidJitEmulator.java @@ -0,0 +1,180 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.wireless.qa.mobileharness.shared.api.device; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ascii; +import com.google.common.flogger.FluentLogger; +import com.google.devtools.mobileharness.api.model.error.MobileHarnessException; +import com.google.devtools.mobileharness.api.model.proto.Device.PostTestDeviceOp; +import com.google.devtools.mobileharness.infra.controller.device.config.ApiConfig; +import com.google.devtools.mobileharness.platform.android.sdktool.adb.AndroidAdbUtil; +import com.google.devtools.mobileharness.platform.android.sdktool.adb.AndroidProperty; +import com.google.devtools.mobileharness.shared.util.command.Command; +import com.google.devtools.mobileharness.shared.util.command.CommandExecutor; +import com.google.devtools.mobileharness.shared.util.command.Timeout; +import com.google.devtools.mobileharness.shared.util.file.local.LocalFileUtil; +import com.google.devtools.mobileharness.shared.util.flags.Flags; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.wireless.qa.mobileharness.shared.api.validator.ValidatorFactory; +import com.google.wireless.qa.mobileharness.shared.model.job.TestInfo; +import java.nio.file.Path; +import java.time.Duration; +import javax.annotation.Nullable; + +/** Android emulator device class. This emulator is created by acloud on demand of test requests. */ +public class AndroidJitEmulator extends AndroidDevice { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String JIT_EMULATOR_IMAGE_ZIP = "JIT_EMULATOR_IMAGE_ZIP"; + public static final String JIT_EMULATOR_HOST_PACKAGE = "JIT_EMULATOR_HOST_PACKAGE"; + public static final String JIT_EMULATOR_ACLOUD = "JIT_EMULATOR_ACLOUD"; + + // TODO: Move to a shared class. + public static final int EMULATOR_BASE_PORT = 6520; + public static final int EMULATOR_INSTANCE_ID_BASE = 1; + + private final String deviceId; + private final AndroidAdbUtil androidAdbUtil; + private final LocalFileUtil localFileUtil; + private final CommandExecutor commandExecutor; + + public AndroidJitEmulator(String deviceId) { + this( + deviceId, + ApiConfig.getInstance(), + new ValidatorFactory(), + new CommandExecutor(), + new AndroidAdbUtil(), + new LocalFileUtil()); + } + + @VisibleForTesting + AndroidJitEmulator( + String deviceId, + @Nullable ApiConfig apiConfig, + @Nullable ValidatorFactory validatorFactory, + CommandExecutor commandExecutor, + AndroidAdbUtil androidAdbUtil, + LocalFileUtil localFileUtil) { + super(deviceId, apiConfig, validatorFactory); + this.deviceId = deviceId; + this.commandExecutor = commandExecutor; + this.androidAdbUtil = androidAdbUtil; + this.localFileUtil = localFileUtil; + } + + @Override + public void setUp() throws MobileHarnessException, InterruptedException { + logger.atInfo().log("JIT emulator start setup. %s", deviceId); + addDimension(Ascii.toLowerCase(AndroidProperty.ABILIST.name()), "x86_64"); + addDimension(Ascii.toLowerCase(AndroidProperty.ABI.name()), "x86_64"); + addSupportedDriver("NoOpDriver"); + addSupportedDriver("AndroidInstrumentation"); + addSupportedDriver("MoblyTest"); + addSupportedDriver("XtsTradefedTest"); + + addSupportedDecorator("AndroidHdVideoDecorator"); + addSupportedDecorator("AndroidDeviceSettingsDecorator"); + addSupportedDecorator("AndroidAdbShellDecorator"); + + addSupportedDeviceType(AndroidJitEmulator.class.getSimpleName()); + addSupportedDeviceType(AndroidDevice.class.getSimpleName()); + basicAndroidDecoratorConfiguration(); + + logger.atInfo().log("JIT emulator %s is Ready", deviceId); + } + + @Override + public void preRunTest(TestInfo testInfo) throws MobileHarnessException, InterruptedException { + String imgZip = testInfo.jobInfo().files().getSingle("device"); + String hostPkg = testInfo.jobInfo().files().getSingle("cvd-host_package.tar.gz"); + Path imageDir = Path.of(testInfo.getTmpFileDir()).resolve("image_dir"); + localFileUtil.prepareDir(imageDir); + + Path hostPkgDir = Path.of(testInfo.getTmpFileDir()).resolve("cvd-host_package"); + String acloudPath = Flags.instance().acloudPath.getNonNull(); + localFileUtil.prepareDir(hostPkgDir); + localFileUtil.unzipFile(imgZip, imageDir.toString()); + + Command unzipHostPkgCmd = Command.of("tar", "-xvzf", hostPkg, "-C", hostPkgDir.toString()); + String unzipHostPkgOutput = commandExecutor.run(unzipHostPkgCmd); + logger.atFine().log("unzipHostPkgOutput: %s", unzipHostPkgOutput); + + localFileUtil.grantFileOrDirFullAccessRecursively(imageDir); + localFileUtil.grantFileOrDirFullAccessRecursively(hostPkgDir); + localFileUtil.grantFileOrDirFullAccess(acloudPath); + + Command command = + Command.of( + acloudPath, + "create", + "--local-instance", + getInstanceId(), + "--local-image", + imageDir.toString(), + "--local-tool", + hostPkgDir.toString(), + "--yes", + "--no-autoconnect", + "--skip-pre-run-check") + .timeout(Timeout.fixed(Duration.ofMinutes(2))); + String output = commandExecutor.run(command); + logger.atInfo().log("acloud create output: %s", output); + String deviceIpAddress = getDeviceId(); + // Attempt to establish an ADB connection to a device's IP address, with retry mechanism + androidAdbUtil.connect(deviceIpAddress); + } + + @CanIgnoreReturnValue + @Override + public PostTestDeviceOp postRunTest(TestInfo testInfo) + throws MobileHarnessException, InterruptedException { + String acloudPath = Flags.instance().acloudPath.getNonNull(); + localFileUtil.grantFileOrDirFullAccess(acloudPath); + Command command = + Command.of(acloudPath, "delete", "--instance-names", "local-instance-" + getInstanceId()); + String output = commandExecutor.run(command); + logger.atInfo().log("acloud delete output: %s", output); + return PostTestDeviceOp.NONE; + } + + @Override + public boolean canReboot() { + return false; + } + + @Override + public void reboot() throws MobileHarnessException, InterruptedException { + logger.atSevere().log("Unexpected attempt to reboot a non-rebootable device"); + } + + @Override + public boolean isRooted() { + // We suppose all emulators are rooted. + return true; + } + + // TODO: Move instance id logic to a shared class to share with + // AndroidJitEmulatorDetector. + private String getInstanceId() { + return String.valueOf( + Integer.parseInt(getDeviceId().substring(getDeviceId().indexOf(':') + 1)) + - EMULATOR_BASE_PORT + + EMULATOR_INSTANCE_ID_BASE); + } +} diff --git a/src/java/com/google/wireless/qa/mobileharness/shared/api/device/BUILD b/src/java/com/google/wireless/qa/mobileharness/shared/api/device/BUILD index c723b066b..3988df994 100644 --- a/src/java/com/google/wireless/qa/mobileharness/shared/api/device/BUILD +++ b/src/java/com/google/wireless/qa/mobileharness/shared/api/device/BUILD @@ -133,6 +133,28 @@ java_library( ], ) +java_library( + name = "android_jit_emulator", + srcs = ["AndroidJitEmulator.java"], + deps = [ + ":android_device", + "//src/devtools/mobileharness/api/model/proto:device_java_proto", + "//src/java/com/google/devtools/mobileharness/api/model/error", + "//src/java/com/google/devtools/mobileharness/infra/controller/device/config", + "//src/java/com/google/devtools/mobileharness/platform/android/sdktool/adb:adb_util", + "//src/java/com/google/devtools/mobileharness/platform/android/sdktool/adb:enums", + "//src/java/com/google/devtools/mobileharness/shared/util/command", + "//src/java/com/google/devtools/mobileharness/shared/util/file/local", + "//src/java/com/google/devtools/mobileharness/shared/util/flags", + "//src/java/com/google/devtools/mobileharness/shared/util/logging:google_logger", + "//src/java/com/google/wireless/qa/mobileharness/shared/api/validator:validator_factory", + "//src/java/com/google/wireless/qa/mobileharness/shared/model/job", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "android_local_emulator", srcs = ["AndroidLocalEmulator.java"],