Add TestApis for using console input
VirtualMachineConfig.setVmConsoleInputSupported: enable console input to
the Vm.
VirtualMachineConfig.isVmConsoleInputSupported: tests whether console
input to the VM is supported or not.
VirtualMachine.getConsoleInput: gets a output stream to the console
input to the VM.
Bug: 263360203
Test: atest MicrodroidTestApp:com.android.microdroid.test.MicrodroidTests#testConsoleInputSupported
Change-Id: I09b1c9c4b216fc887f747fd6cb9712c7b66b8754
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 8b7ec11..1298000 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -2,15 +2,18 @@
package android.system.virtualmachine {
public class VirtualMachine implements java.lang.AutoCloseable {
+ method @NonNull @WorkerThread public java.io.OutputStream getConsoleInput() throws android.system.virtualmachine.VirtualMachineException;
method @NonNull public java.io.File getRootDir();
}
public final class VirtualMachineConfig {
method @Nullable public String getPayloadConfigPath();
+ method public boolean isVmConsoleInputSupported();
}
public static final class VirtualMachineConfig.Builder {
method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmConsoleInputSupported(boolean);
}
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 19f57fb..675a046 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -76,11 +76,13 @@
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.channels.FileChannel;
@@ -294,6 +296,8 @@
private final boolean mVmOutputCaptured;
+ private final boolean mVmConsoleInputSupported;
+
/** The configuration that is currently associated with this VM. */
@GuardedBy("mLock")
@NonNull
@@ -306,11 +310,19 @@
@GuardedBy("mLock")
@Nullable
- private ParcelFileDescriptor mConsoleReader;
+ private ParcelFileDescriptor mConsoleOutReader;
@GuardedBy("mLock")
@Nullable
- private ParcelFileDescriptor mConsoleWriter;
+ private ParcelFileDescriptor mConsoleOutWriter;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mConsoleInReader;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mConsoleInWriter;
@GuardedBy("mLock")
@Nullable
@@ -372,6 +384,7 @@
: null;
mVmOutputCaptured = config.isVmOutputCaptured();
+ mVmConsoleInputSupported = config.isVmConsoleInputSupported();
}
/**
@@ -787,7 +800,11 @@
try {
if (mVmOutputCaptured) {
- createVmPipes();
+ createVmOutputPipes();
+ }
+
+ if (mVmConsoleInputSupported) {
+ createVmInputPipes();
}
VirtualMachineAppConfig appConfig =
@@ -804,8 +821,9 @@
android.system.virtualizationservice.VirtualMachineConfig.appConfig(
appConfig);
- // TODO(b/263360203): use consoleInFd also in Java (via private APIs)
- mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter, /* consoleInFd */ null, mLogWriter);
+ mVirtualMachine =
+ service.createVm(
+ vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
mVirtualMachine.registerCallback(new CallbackTranslator(service));
mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
mVirtualMachine.start();
@@ -844,12 +862,12 @@
}
@GuardedBy("mLock")
- private void createVmPipes() throws VirtualMachineException {
+ private void createVmOutputPipes() throws VirtualMachineException {
try {
- if (mConsoleReader == null || mConsoleWriter == null) {
+ if (mConsoleOutReader == null || mConsoleOutWriter == null) {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
- mConsoleReader = pipe[0];
- mConsoleWriter = pipe[1];
+ mConsoleOutReader = pipe[0];
+ mConsoleOutWriter = pipe[1];
}
if (mLogReader == null || mLogWriter == null) {
@@ -858,7 +876,20 @@
mLogWriter = pipe[1];
}
} catch (IOException e) {
- throw new VirtualMachineException("Failed to create stream for VM", e);
+ throw new VirtualMachineException("Failed to create output stream for VM", e);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void createVmInputPipes() throws VirtualMachineException {
+ try {
+ if (mConsoleInReader == null || mConsoleInWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mConsoleInReader = pipe[0];
+ mConsoleInWriter = pipe[1];
+ }
+ } catch (IOException e) {
+ throw new VirtualMachineException("Failed to create input stream for VM", e);
}
}
@@ -884,12 +915,37 @@
throw new VirtualMachineException("Capturing vm outputs is turned off");
}
synchronized (mLock) {
- createVmPipes();
- return new FileInputStream(mConsoleReader.getFileDescriptor());
+ createVmOutputPipes();
+ return new FileInputStream(mConsoleOutReader.getFileDescriptor());
}
}
/**
+ * Returns the stream object representing the console input to the virtual machine. The console
+ * input is only available if the {@link VirtualMachineConfig} specifies that it should be
+ * {@linkplain VirtualMachineConfig#isVmConsoleInputSupported supported}.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the stream could not be created, or console input is not
+ * supported.
+ * @hide
+ */
+ @TestApi
+ @WorkerThread
+ @NonNull
+ public OutputStream getConsoleInput() throws VirtualMachineException {
+ if (!mVmConsoleInputSupported) {
+ throw new VirtualMachineException("VM console input is not supported");
+ }
+ synchronized (mLock) {
+ createVmInputPipes();
+ return new FileOutputStream(mConsoleInWriter.getFileDescriptor());
+ }
+ }
+
+
+ /**
* Returns the stream object representing the log output from the virtual machine. The log
* output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
* VirtualMachineConfig#isVmOutputCaptured captured}.
@@ -911,7 +967,7 @@
throw new VirtualMachineException("Capturing vm outputs is turned off");
}
synchronized (mLock) {
- createVmPipes();
+ createVmOutputPipes();
return new FileInputStream(mLogReader.getFileDescriptor());
}
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 93e65db..2ce2b62 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -76,6 +76,7 @@
private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
+ private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -164,6 +165,9 @@
/** Whether the app can read console and log output. */
private final boolean mVmOutputCaptured;
+ /** Whether the app can write console input to the VM */
+ private final boolean mVmConsoleInputSupported;
+
private VirtualMachineConfig(
@Nullable String packageName,
@Nullable String apkPath,
@@ -174,7 +178,8 @@
long memoryBytes,
@CpuTopology int cpuTopology,
long encryptedStorageBytes,
- boolean vmOutputCaptured) {
+ boolean vmOutputCaptured,
+ boolean vmConsoleInputSupported) {
// This is only called from Builder.build(); the builder handles parameter validation.
mPackageName = packageName;
mApkPath = apkPath;
@@ -186,6 +191,7 @@
mCpuTopology = cpuTopology;
mEncryptedStorageBytes = encryptedStorageBytes;
mVmOutputCaptured = vmOutputCaptured;
+ mVmConsoleInputSupported = vmConsoleInputSupported;
}
/** Loads a config from a file. */
@@ -260,6 +266,7 @@
builder.setEncryptedStorageBytes(encryptedStorageBytes);
}
builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
+ builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
return builder.build();
}
@@ -295,6 +302,7 @@
b.putLong(KEY_ENCRYPTED_STORAGE_BYTES, mEncryptedStorageBytes);
}
b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
+ b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
b.writeToStream(output);
}
@@ -413,6 +421,17 @@
}
/**
+ * Returns whether the app can write to the VM console.
+ *
+ * @see Builder#setVmConsoleInputSupported
+ * @hide
+ */
+ @TestApi
+ public boolean isVmConsoleInputSupported() {
+ return mVmConsoleInputSupported;
+ }
+
+ /**
* Tests if this config is compatible with other config. Being compatible means that the configs
* can be interchangeably used for the same virtual machine; they do not change the VM identity
* or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
@@ -431,6 +450,7 @@
&& this.mProtectedVm == other.mProtectedVm
&& this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
&& this.mVmOutputCaptured == other.mVmOutputCaptured
+ && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
&& Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
&& Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
&& Objects.equals(this.mPackageName, other.mPackageName)
@@ -554,6 +574,7 @@
@CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
private long mEncryptedStorageBytes;
private boolean mVmOutputCaptured = false;
+ private boolean mVmConsoleInputSupported = false;
/**
* Creates a builder for the given context.
@@ -612,6 +633,10 @@
throw new IllegalStateException("debug level must be FULL to capture output");
}
+ if (mVmConsoleInputSupported && mDebugLevel != DEBUG_LEVEL_FULL) {
+ throw new IllegalStateException("debug level must be FULL to use console input");
+ }
+
return new VirtualMachineConfig(
packageName,
apkPath,
@@ -622,7 +647,8 @@
mMemoryBytes,
mCpuTopology,
mEncryptedStorageBytes,
- mVmOutputCaptured);
+ mVmOutputCaptured,
+ mVmConsoleInputSupported);
}
/**
@@ -822,5 +848,23 @@
mVmOutputCaptured = captured;
return this;
}
+
+ /**
+ * Sets whether to allow the app to write to the VM console. Default is {@code false}.
+ *
+ * <p>Setting this as {@code true} will allow the app to directly write into {@linkplain
+ * VirtualMachine#getConsoleInput console input}.
+ *
+ * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+ * set as true.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public Builder setVmConsoleInputSupported(boolean supported) {
+ mVmConsoleInputSupported = supported;
+ return this;
+ }
}
}
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index a6f1c80..8d467cd 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -70,6 +70,9 @@
/** Requests the VM to asynchronously call appCallback.setVmCallback() */
void requestCallback(IAppCallback appCallback);
+ /** Read a line from /dev/console */
+ String readLineFromConsole();
+
/**
* Request the service to exit, triggering the termination of the VM. This may cause any
* requests in flight to fail.
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index f58ce81..d30f0d0 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -471,6 +471,7 @@
public long[] mTimings;
public int mFileMode;
public int mMountFlags;
+ public String mConsoleInput;
public void assertNoException() {
if (mException != null) {
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 a64b62a..ffb2c11 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1652,6 +1652,35 @@
}
@Test
+ public void testConsoleInputSupported() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setVmConsoleInputSupported(true)
+ .setVmOutputCaptured(true)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_console_in", config);
+
+ final String TYPED = "this is a console input\n";
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ OutputStreamWriter consoleIn =
+ new OutputStreamWriter(vm.getConsoleInput());
+ consoleIn.write(TYPED);
+ consoleIn.close();
+ tr.mConsoleInput = ts.readLineFromConsole();
+ });
+ testResults.assertNoException();
+ assertThat(testResults.mConsoleInput).isEqualTo(TYPED);
+ }
+
+ @Test
public void testStartVmWithPayloadOfAnotherApp() throws Exception {
assumeSupportedDevice();
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 7e0fc5b..297b505 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -313,6 +313,26 @@
return ScopedAStatus::ok();
}
+ ScopedAStatus readLineFromConsole(std::string* out) {
+ FILE* f = fopen("/dev/console", "r");
+ if (f == nullptr) {
+ return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+ "failed to open /dev/console");
+ }
+ char* line = nullptr;
+ size_t len = 0;
+ ssize_t nread = getline(&line, &len, f);
+
+ if (nread == -1) {
+ free(line);
+ return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+ "failed to read /dev/console");
+ }
+ out->append(line, nread);
+ free(line);
+ return ScopedAStatus::ok();
+ }
+
ScopedAStatus quit() override { exit(0); }
};
auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 1f7ffb7..0ddf70b 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -245,6 +245,11 @@
}
@Override
+ public String readLineFromConsole() {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
public void quit() throws RemoteException {
throw new UnsupportedOperationException("Not supported");
}