Merge "[VM] Build virtual machine from a descriptor"
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 6233e0a..1aa8949 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -16,6 +16,7 @@
package android.system.virtualmachine;
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
@@ -69,12 +70,14 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.nio.channels.FileChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
@@ -284,10 +287,45 @@
return sInstances.computeIfAbsent(context, unused -> new HashMap<>());
}
+ /**
+ * Builds a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+ * with the given name.
+ *
+ * <p>The new virtual machine will be in the same state as the descriptor indicates.
+ *
+ * <p>Once a virtual machine is imported it is persisted until it is deleted by calling {@link
+ * #delete}. The imported virtual machine is in {@link #STATUS_STOPPED} state. To run the VM,
+ * call {@link #run}.
+ */
+ @GuardedBy("sCreateLock")
@NonNull
- private static File getVmDir(Context context, String name) {
- File vmRoot = new File(context.getDataDir(), VM_DIR);
- return new File(vmRoot, name);
+ static VirtualMachine fromDescriptor(
+ @NonNull Context context,
+ @NonNull String name,
+ @NonNull VirtualMachineDescriptor vmDescriptor)
+ throws VirtualMachineException {
+ VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
+ File vmDir = createVmDir(context, name);
+ try {
+ VirtualMachine vm = new VirtualMachine(context, name, config);
+ config.serialize(vm.mConfigFilePath);
+ try {
+ vm.mInstanceFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to create instance image", e);
+ }
+ vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
+ getInstancesMap(context).put(name, new WeakReference<>(vm));
+ return vm;
+ } catch (VirtualMachineException | RuntimeException e) {
+ // If anything goes wrong, delete any files created so far and the VM's directory
+ try {
+ deleteRecursively(vmDir);
+ } catch (IOException innerException) {
+ e.addSuppressed(innerException);
+ }
+ throw e;
+ }
}
/**
@@ -300,21 +338,7 @@
static VirtualMachine create(
@NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
throws VirtualMachineException {
- File vmDir = getVmDir(context, name);
-
- try {
- // We don't need to undo this even if VM creation fails.
- Files.createDirectories(vmDir.getParentFile().toPath());
-
- // The checking of the existence of this directory and the creation of it is done
- // atomically. If the directory already exists (i.e. the VM with the same name was
- // already created), FileAlreadyExistsException is thrown.
- Files.createDirectory(vmDir.toPath());
- } catch (FileAlreadyExistsException e) {
- throw new VirtualMachineException("virtual machine already exists", e);
- } catch (IOException e) {
- throw new VirtualMachineException("failed to create directory for VM", e);
- }
+ File vmDir = createVmDir(context, name);
try {
VirtualMachine vm = new VirtualMachine(context, name, config);
@@ -412,6 +436,33 @@
if (instancesMap != null) instancesMap.remove(name);
}
+ @GuardedBy("sCreateLock")
+ @NonNull
+ private static File createVmDir(@NonNull Context context, @NonNull String name)
+ throws VirtualMachineException {
+ File vmDir = getVmDir(context, name);
+ try {
+ // We don't need to undo this even if VM creation fails.
+ Files.createDirectories(vmDir.getParentFile().toPath());
+
+ // The checking of the existence of this directory and the creation of it is done
+ // atomically. If the directory already exists (i.e. the VM with the same name was
+ // already created), FileAlreadyExistsException is thrown.
+ Files.createDirectory(vmDir.toPath());
+ } catch (FileAlreadyExistsException e) {
+ throw new VirtualMachineException("virtual machine already exists", e);
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to create directory for VM", e);
+ }
+ return vmDir;
+ }
+
+ @NonNull
+ private static File getVmDir(Context context, String name) {
+ File vmRoot = new File(context.getDataDir(), VM_DIR);
+ return new File(vmRoot, name);
+ }
+
/**
* Returns the name of this virtual machine. The name is unique in the package and can't be
* changed.
@@ -1053,4 +1104,14 @@
throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
}
}
+
+ private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
+ throws VirtualMachineException {
+ try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
+ FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
+ instance.transferFrom(instanceInput, /*position=*/ 0, instanceInput.size());
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to transfer instance image", e);
+ }
+ }
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 593a57d..a660306 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -16,6 +16,7 @@
package android.system.virtualmachine;
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static java.util.Objects.requireNonNull;
@@ -193,6 +194,17 @@
}
}
+ /** Loads a config from a {@link ParcelFileDescriptor}. */
+ @NonNull
+ static VirtualMachineConfig from(@NonNull ParcelFileDescriptor fd)
+ throws VirtualMachineException {
+ try (AutoCloseInputStream input = new AutoCloseInputStream(fd)) {
+ return fromInputStream(input);
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to read VM config from file descriptor", e);
+ }
+ }
+
/** Loads a config from a stream, for example a file. */
@NonNull
private static VirtualMachineConfig fromInputStream(@NonNull InputStream input)
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 70532fc..6590695 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -23,8 +23,6 @@
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
-import com.android.internal.annotations.VisibleForTesting;
-
/**
* A VM descriptor that captures the state of a Virtual Machine.
*
@@ -65,8 +63,8 @@
* @return File descriptor of the VM configuration file config.xml.
* @hide
*/
- @VisibleForTesting
- public @NonNull ParcelFileDescriptor getConfigFd() {
+ @NonNull
+ ParcelFileDescriptor getConfigFd() {
return mConfigFd;
}
@@ -74,8 +72,8 @@
* @return File descriptor of the instance.img of the VM.
* @hide
*/
- @VisibleForTesting
- public @NonNull ParcelFileDescriptor getInstanceImgFd() {
+ @NonNull
+ ParcelFileDescriptor getInstanceImgFd() {
return mInstanceImgFd;
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 34b9fd9..c357f50 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -142,6 +142,24 @@
}
/**
+ * Imports a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+ * with the given name.
+ *
+ * <p>The new virtual machine will be in the same state as the descriptor indicates.
+ *
+ * @throws VirtualMachineException if the VM cannot be imported.
+ * @hide
+ */
+ @NonNull
+ public VirtualMachine importFromDescriptor(
+ @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
+ throws VirtualMachineException {
+ synchronized (VirtualMachine.sCreateLock) {
+ return VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+ }
+ }
+
+ /**
* Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
* such virtual machine.
*
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 492eb33..5e86798 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -61,6 +61,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.List;
import java.util.OptionalLong;
import java.util.UUID;
@@ -600,31 +601,46 @@
}
@Test
- public void vmConvertsToValidDescriptor() throws Exception {
+ public void importedVmIsEqualToTheOriginalVm() throws Exception {
// Arrange
VirtualMachineConfig config =
mInner.newVmConfigBuilder()
.setPayloadBinaryPath("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
.build();
- String vmName = "test_vm";
- VirtualMachine vm = mInner.forceCreateNewVirtualMachine(vmName, config);
+ String vmNameOrig = "test_vm_orig", vmNameImport = "test_vm_import";
+ VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+ // Run something to make the instance.img different with the initialized one.
+ TestResults origTestResults = runVmTestService(vmOrig);
+ assertThat(origTestResults.mException).isNull();
+ assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
+ VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
+ VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+ if (vmm.get(vmNameImport) != null) {
+ vmm.delete(vmNameImport);
+ }
// Action
- VirtualMachineDescriptor descriptor = vm.toDescriptor();
+ VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
// Asserts
- assertFileContentsAreEqual(descriptor.getConfigFd(), vmName, "config.xml");
- assertFileContentsAreEqual(descriptor.getInstanceImgFd(), vmName, "instance.img");
+ assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport);
+ assertFileContentsAreEqualInTwoVms("instance.img", vmNameOrig, vmNameImport);
+ assertThat(vmImport).isNotEqualTo(vmOrig);
+ vmm.delete(vmNameOrig);
+ assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
+ TestResults testResults = runVmTestService(vmImport);
+ assertThat(testResults.mException).isNull();
+ assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
}
- private void assertFileContentsAreEqual(
- ParcelFileDescriptor parcelFd, String vmName, String fileName) throws IOException {
- File file = getVmFile(vmName, fileName);
- // Use try-with-resources to close the files automatically after assert.
- try (FileInputStream input1 = new FileInputStream(parcelFd.getFileDescriptor());
- FileInputStream input2 = new FileInputStream(file)) {
- assertThat(input1.readAllBytes()).isEqualTo(input2.readAllBytes());
+ private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
+ throws IOException {
+ File file1 = getVmFile(vmName1, fileName);
+ File file2 = getVmFile(vmName2, fileName);
+ try (FileInputStream input1 = new FileInputStream(file1);
+ FileInputStream input2 = new FileInputStream(file2)) {
+ assertThat(Arrays.equals(input1.readAllBytes(), input2.readAllBytes())).isTrue();
}
}