Guest OS serial console
VirtualMachine library changes:
Add a new debug config "connectVmConsole".
If it is true then AVF would create a pty (pseudoterminal) on host and
connect it to the VM serial console.
If "vmOutputCaptured" is also true, then a worker thread would be
submitted to the background and tee the VM console output to both
VirtualMachine#getConsoleOutput() and the host pty.
VirtualMachine#getHostConsoleName() to get the name of the peer end of
the pty (aka. ptsname) which usually looks like `/dev/pts/<num>`.
The number of the pts node is not fixed as it is dynamically allocated
by the pty driver.
You can then connect to the serial console through adb:
```
$ # Get the readonly logs only
$ adb shell cat /dev/pts/0
$ # Connect to console with microcom; exit with `ctrl-]` then `q`
$ adb shell -t microcom /dev/pts/0
$ # Or with netcat
$ adb shell -t 'stty raw -echo && netcat -f /dev/pts/0'
```
Keep in mind that special characters like ctrl-c and ctrl-z are _not_
intepreted, so you would not be able to interrupt the host netcat
command by typing ctrl-c. Pressing ctrl-c would send the ctrl-c keycode
to the other end of the serial console, and cause an interrupt in the
guest VM console.
If you want to be able to interrupt and stop the host netcat command,
pass the `isig` terminal option:
```
adb shell -t 'stty raw isig -echo && netcat -f /dev/pts/0'
```
Vmlauncher APP changes:
Don't flood the host logcat with guest VM console logs.
Instead direct it to a file:
/data/user/0/com.android.virtualization.vmlauncher/files/console.log
User can also connect to the host pts to connect to the VM console.
BYPASS_INCLUSIVE_LANGUAGE_REASON=It is well known that "man" stands for
the "[man]ual" command in the unix world.
Bug: 335362012
Test: Start a vm and then `adb shell -t microcom /dev/pts/0`
Change-Id: I9179510b237b8a19b8da7c527f3f42846a127dd3
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 2f6e306..43f3db0 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -42,8 +42,8 @@
import static java.util.Objects.requireNonNull;
-import android.annotation.FlaggedApi;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -63,8 +63,8 @@
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
+import android.system.ErrnoException;
+import android.system.OsConstants;
import android.system.virtualizationcommon.DeathReason;
import android.system.virtualizationcommon.ErrorCode;
import android.system.virtualizationservice.IVirtualMachine;
@@ -78,13 +78,17 @@
import android.system.virtualizationservice.VirtualMachineState;
import android.util.JsonReader;
import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
import com.android.internal.annotations.GuardedBy;
import com.android.system.virtualmachine.flags.Flags;
import libcore.io.IoBridge;
+import libcore.io.IoUtils;
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -97,17 +101,20 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Arrays;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.zip.ZipFile;
@@ -320,6 +327,10 @@
private final boolean mVmConsoleInputSupported;
+ private final boolean mConnectVmConsole;
+
+ private final Executor mConsoleExecutor = Executors.newSingleThreadExecutor();
+
/** The configuration that is currently associated with this VM. */
@GuardedBy("mLock")
@NonNull
@@ -348,6 +359,26 @@
@GuardedBy("mLock")
@Nullable
+ private ParcelFileDescriptor mTeeConsoleOutReader;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mTeeConsoleOutWriter;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mPtyFd;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mPtsFd;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private String mPtsName;
+
+ @GuardedBy("mLock")
+ @Nullable
private ParcelFileDescriptor mLogReader;
@GuardedBy("mLock")
@@ -417,6 +448,7 @@
mVmOutputCaptured = config.isVmOutputCaptured();
mVmConsoleInputSupported = config.isVmConsoleInputSupported();
+ mConnectVmConsole = config.isConnectVmConsole();
}
/**
@@ -1128,6 +1160,10 @@
IVirtualizationService service = mVirtualizationService.getBinder();
try {
+ if (mConnectVmConsole) {
+ createPtyConsole();
+ }
+
if (mVmOutputCaptured) {
createVmOutputPipes();
}
@@ -1136,6 +1172,38 @@
createVmInputPipes();
}
+ ParcelFileDescriptor consoleOutFd = null;
+ if (mConnectVmConsole && mVmOutputCaptured) {
+ // If we are enabling output pipes AND the host console, then we tee the console
+ // output to both.
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mTeeConsoleOutReader = pipe[0];
+ mTeeConsoleOutWriter = pipe[1];
+ consoleOutFd = mTeeConsoleOutWriter;
+ TeeWorker tee =
+ new TeeWorker(
+ mName + " console",
+ new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
+ List.of(
+ new FileOutputStream(mPtyFd.getFileDescriptor()),
+ new FileOutputStream(
+ mConsoleOutWriter.getFileDescriptor())));
+ // If the VM is stopped then the tee worker thread would get an EOF or read()
+ // error which would tear down itself.
+ mConsoleExecutor.execute(tee);
+ } else if (mConnectVmConsole) {
+ consoleOutFd = mPtyFd;
+ } else if (mVmOutputCaptured) {
+ consoleOutFd = mConsoleOutWriter;
+ }
+
+ ParcelFileDescriptor consoleInFd = null;
+ if (mConnectVmConsole) {
+ consoleInFd = mPtyFd;
+ } else if (mVmConsoleInputSupported) {
+ consoleInFd = mConsoleInReader;
+ }
+
VirtualMachineConfig vmConfig = getConfig();
android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
vmConfig.getCustomImageConfig() != null
@@ -1143,8 +1211,7 @@
: createVirtualMachineConfigForAppFrom(vmConfig, service);
mVirtualMachine =
- service.createVm(
- vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
+ service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
mVirtualMachine.registerCallback(new CallbackTranslator(service));
mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
mVirtualMachine.start();
@@ -1203,15 +1270,81 @@
private void createVmInputPipes() throws VirtualMachineException {
try {
if (mConsoleInReader == null || mConsoleInWriter == null) {
- ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
- mConsoleInReader = pipe[0];
- mConsoleInWriter = pipe[1];
+ if (mConnectVmConsole) {
+ // If we are enabling input pipes AND the host console, then we should just use
+ // the host pty peer end as the console write end.
+ createPtyConsole();
+ mConsoleInReader = mPtyFd.dup();
+ mConsoleInWriter = mPtsFd.dup();
+ } else {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mConsoleInReader = pipe[0];
+ mConsoleInWriter = pipe[1];
+ }
}
} catch (IOException e) {
throw new VirtualMachineException("Failed to create input stream for VM", e);
}
}
+ @FunctionalInterface
+ private static interface OpenPtyCallback {
+ public void apply(FileDescriptor mfd, FileDescriptor sfd, byte[] name);
+ }
+
+ // Opens a pty and set the master end to raw mode and O_NONBLOCK.
+ private static native void nativeOpenPtyRawNonblock(OpenPtyCallback resultCallback)
+ throws IOException;
+
+ @GuardedBy("mLock")
+ private void createPtyConsole() throws VirtualMachineException {
+ if (mPtyFd != null && mPtsFd != null) {
+ return;
+ }
+ List<FileDescriptor> fd = new ArrayList<>(2);
+ StringBuilder nameBuilder = new StringBuilder();
+ try {
+ try {
+ nativeOpenPtyRawNonblock(
+ (FileDescriptor mfd, FileDescriptor sfd, byte[] ptsName) -> {
+ fd.add(mfd);
+ fd.add(sfd);
+ nameBuilder.append(new String(ptsName, StandardCharsets.UTF_8));
+ });
+ } catch (Exception e) {
+ fd.forEach(IoUtils::closeQuietly);
+ throw e;
+ }
+ } catch (IOException e) {
+ throw new VirtualMachineException(
+ "Failed to create host console to connect to the VM console", e);
+ }
+ mPtyFd = new ParcelFileDescriptor(fd.get(0));
+ mPtsFd = new ParcelFileDescriptor(fd.get(1));
+ mPtsName = nameBuilder.toString();
+ Log.d(TAG, "Serial console device: " + mPtsName);
+ }
+
+ /**
+ * Returns the name of the peer end (ptsname) of the host console. The host console is only
+ * available if the {@link VirtualMachineConfig} specifies that a host console should
+ * {@linkplain VirtualMachineConfig#isConnectVmConsole connect} to the VM console.
+ *
+ * @throws VirtualMachineException if the host pseudoterminal could not be created, or
+ * connecting to the VM console is not enabled.
+ * @hide
+ */
+ @NonNull
+ public String getHostConsoleName() throws VirtualMachineException {
+ if (!mConnectVmConsole) {
+ throw new VirtualMachineException("Host console is not enabled");
+ }
+ synchronized (mLock) {
+ createPtyConsole();
+ return mPtsName;
+ }
+ }
+
/**
* Returns the stream object representing the console output from the virtual machine. The
* console output is only available if the {@link VirtualMachineConfig} specifies that it should
@@ -1811,4 +1944,61 @@
}
}
}
+
+ /**
+ * Duplicates {@code InputStream} data to multiple {@code OutputStream}. Like the "tee" command.
+ *
+ * <p>Supports non-blocking writes to the output streams by ignoring EAGAIN error.
+ */
+ private static class TeeWorker implements Runnable {
+ private final String mName;
+ private final InputStream mIn;
+ private final List<OutputStream> mOuts;
+
+ TeeWorker(String name, InputStream in, Collection<OutputStream> outs) {
+ mName = name;
+ mIn = in;
+ mOuts = new ArrayList<>(outs);
+ }
+
+ @Override
+ public void run() {
+ byte[] buffer = new byte[2048];
+ try {
+ while (!Thread.interrupted()) {
+ int len = mIn.read(buffer);
+ if (len < 0) {
+ break;
+ }
+ for (OutputStream out : mOuts) {
+ try {
+ out.write(buffer, 0, len);
+ } catch (IOException e) {
+ // EAGAIN is expected because the file description has O_NONBLOCK flag.
+ if (!isErrnoError(e, OsConstants.EAGAIN)) {
+ throw e;
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Tee " + mName, e);
+ }
+ }
+
+ private static ErrnoException asErrnoException(Throwable e) {
+ if (e instanceof ErrnoException) {
+ return (ErrnoException) e;
+ } else if (e instanceof IOException) {
+ // Try to unwrap ErrnoException#rethrowAsIOException()
+ return asErrnoException(e.getCause());
+ }
+ return null;
+ }
+
+ private static boolean isErrnoError(Exception e, int expectedValue) {
+ ErrnoException errno = asErrnoException(e);
+ return errno != null && errno.errno == expectedValue;
+ }
+ }
}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 1b915cd..66d0f4b 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -48,7 +48,6 @@
import com.android.system.virtualmachine.flags.Flags;
-
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -94,6 +93,7 @@
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";
+ private static final String KEY_CONNECT_VM_CONSOLE = "connectVmConsole";
private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
private static final String KEY_OS = "os";
private static final String KEY_EXTRA_APKS = "extraApks";
@@ -193,6 +193,9 @@
/** Whether the app can write console input to the VM */
private final boolean mVmConsoleInputSupported;
+ /** Whether to connect the VM console to a host console. */
+ private final boolean mConnectVmConsole;
+
@Nullable private final File mVendorDiskImage;
/** OS name of the VM using payload binaries. */
@@ -229,6 +232,7 @@
long encryptedStorageBytes,
boolean vmOutputCaptured,
boolean vmConsoleInputSupported,
+ boolean connectVmConsole,
@Nullable File vendorDiskImage,
@NonNull @OsName String os) {
// This is only called from Builder.build(); the builder handles parameter validation.
@@ -249,6 +253,7 @@
mEncryptedStorageBytes = encryptedStorageBytes;
mVmOutputCaptured = vmOutputCaptured;
mVmConsoleInputSupported = vmConsoleInputSupported;
+ mConnectVmConsole = connectVmConsole;
mVendorDiskImage = vendorDiskImage;
mOs = os;
}
@@ -331,6 +336,7 @@
}
builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+ builder.setConnectVmConsole(b.getBoolean(KEY_CONNECT_VM_CONSOLE));
String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
if (vendorDiskImagePath != null) {
@@ -384,6 +390,7 @@
}
b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+ b.putBoolean(KEY_CONNECT_VM_CONSOLE, mConnectVmConsole);
if (mVendorDiskImage != null) {
b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
}
@@ -544,6 +551,16 @@
}
/**
+ * Returns whether to connect the VM console to a host console.
+ *
+ * @see Builder#setConnectVmConsole
+ * @hide
+ */
+ public boolean isConnectVmConsole() {
+ return mConnectVmConsole;
+ }
+
+ /**
* Returns the OS of the VM.
*
* @see Builder#setOs
@@ -577,6 +594,7 @@
&& this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
&& this.mVmOutputCaptured == other.mVmOutputCaptured
&& this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
+ && this.mConnectVmConsole == other.mConnectVmConsole
&& (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
&& Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
&& Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
@@ -789,6 +807,7 @@
private long mEncryptedStorageBytes;
private boolean mVmOutputCaptured = false;
private boolean mVmConsoleInputSupported = false;
+ private boolean mConnectVmConsole = false;
@Nullable private File mVendorDiskImage;
@NonNull @OsName private String mOs = DEFAULT_OS;
@@ -862,6 +881,11 @@
throw new IllegalStateException("debug level must be FULL to use console input");
}
+ if (mConnectVmConsole && mDebugLevel != DEBUG_LEVEL_FULL) {
+ throw new IllegalStateException(
+ "debug level must be FULL to connect to the console");
+ }
+
return new VirtualMachineConfig(
packageName,
apkPath,
@@ -876,6 +900,7 @@
mEncryptedStorageBytes,
mVmOutputCaptured,
mVmConsoleInputSupported,
+ mConnectVmConsole,
mVendorDiskImage,
mOs);
}
@@ -1125,6 +1150,23 @@
}
/**
+ * Sets whether to connect the VM console to a host console. Default is {@code false}.
+ *
+ * <p>Setting this as {@code true} will allow the shell to directly communicate with the VM
+ * console through the connected host console.
+ *
+ * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+ * set as true.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setConnectVmConsole(boolean supported) {
+ mConnectVmConsole = supported;
+ return this;
+ }
+
+ /**
* Sets the path to the disk image with vendor-specific modules.
*
* @hide
diff --git a/java/jni/android_system_virtualmachine_VirtualMachine.cpp b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
index b3354cc..ed102bf 100644
--- a/java/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -17,16 +17,36 @@
#define LOG_TAG "VirtualMachine"
#include <aidl/android/system/virtualizationservice/IVirtualMachine.h>
+#include <android-base/scopeguard.h>
+#include <android-base/strings.h>
#include <android/binder_auto_utils.h>
#include <android/binder_ibinder_jni.h>
+#include <fcntl.h>
#include <jni.h>
#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <pty.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <termios.h>
+#include <unistd.h>
#include <binder_rpc_unstable.hpp>
+#include <string>
#include <tuple>
#include "common.h"
+namespace {
+
+void throwIOException(JNIEnv *env, const std::string &msg) {
+ jniThrowException(env, "java/io/IOException", msg.c_str());
+}
+
+} // namespace
+
extern "C" JNIEXPORT jobject JNICALL
Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
@@ -65,3 +85,72 @@
auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
return AIBinder_toJavaBinder(env, client);
}
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_system_virtualmachine_VirtualMachine_nativeOpenPtyRawNonblock(
+ JNIEnv *env, [[maybe_unused]] jclass clazz, jobject resultCallback) {
+ int pm, ps;
+ // man openpty says: "Nobody knows how much space should be reserved for name."
+ // but on modern Linux the format of the pts name is always `/dev/pts/[0-9]+`
+ // Realistically speaking, a buffer of 32 bytes leaves us with 22 digits for the pts number,
+ // which should be more than enough.
+ // NOTE: bionic implements openpty() with internal name buffer of size 32, musl 20.
+ char name[32];
+ if (openpty(&pm, &ps, name, nullptr, nullptr)) {
+ throwIOException(env, "openpty(): " + android::base::ErrnoNumberAsString(errno));
+ return;
+ }
+ fcntl(pm, F_SETFD, FD_CLOEXEC);
+ fcntl(ps, F_SETFD, FD_CLOEXEC);
+ name[sizeof(name) - 1] = '\0';
+ // Set world RW so adb shell can talk to the pts.
+ chmod(name, 0666);
+
+ if (int flags = fcntl(pm, F_GETFL, 0); flags < 0) {
+ throwIOException(env, "fcntl(F_GETFL): " + android::base::ErrnoNumberAsString(errno));
+ return;
+ } else if (fcntl(pm, F_SETFL, flags | O_NONBLOCK) < 0) {
+ throwIOException(env, "fcntl(F_SETFL): " + android::base::ErrnoNumberAsString(errno));
+ return;
+ }
+
+ android::base::ScopeGuard cleanup_handler([=] {
+ close(ps);
+ close(pm);
+ });
+
+ struct termios tio;
+ if (tcgetattr(pm, &tio)) {
+ throwIOException(env, "tcgetattr(): " + android::base::ErrnoNumberAsString(errno));
+ return;
+ }
+ cfmakeraw(&tio);
+ if (tcsetattr(pm, TCSANOW, &tio)) {
+ throwIOException(env, "tcsetattr(): " + android::base::ErrnoNumberAsString(errno));
+ return;
+ }
+
+ jobject mfd = jniCreateFileDescriptor(env, pm);
+ if (mfd == nullptr) {
+ return;
+ }
+ jobject sfd = jniCreateFileDescriptor(env, ps);
+ if (sfd == nullptr) {
+ return;
+ }
+ size_t len = strlen(name);
+ ScopedLocalRef<jbyteArray> ptsName(env, env->NewByteArray(len));
+ if (ptsName.get() != nullptr) {
+ env->SetByteArrayRegion(ptsName.get(), 0, len, (jbyte *)name);
+ }
+ ScopedLocalRef<jclass> callback_class(env, env->GetObjectClass(resultCallback));
+ jmethodID mid = env->GetMethodID(callback_class.get(), "apply",
+ "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;[B)V");
+ if (mid == nullptr) {
+ return;
+ }
+
+ env->CallVoidMethod(resultCallback, mid, mfd, sfd, ptsName.get());
+ // FD ownership is transferred to the callback, reset the auto-close hander.
+ cleanup_handler.Disable();
+}
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 10f8bf6..521e2f1 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,40 +19,43 @@
import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
import android.app.Activity;
+import android.crosvm.ICrosvmAndroidDisplayService;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.crosvm.ICrosvmAndroidDisplayService;
import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
-import android.util.DisplayMetrics;
-import android.util.Log;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineCallback;
import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
import android.system.virtualmachine.VirtualMachineException;
import android.system.virtualmachine.VirtualMachineManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
+import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
-import android.view.KeyEvent;
import android.view.View;
-import android.view.WindowManager;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
+import android.view.WindowManager;
import android.view.WindowMetrics;
+
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
@@ -63,7 +66,7 @@
private static final String TAG = "VmLauncherApp";
private static final String VM_NAME = "my_custom_vm";
private static final boolean DEBUG = true;
- private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
+ private ExecutorService mExecutorService;
private VirtualMachine mVirtualMachine;
private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
@@ -75,6 +78,7 @@
if (DEBUG) {
configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
configBuilder.setVmOutputCaptured(true);
+ configBuilder.setConnectVmConsole(true);
}
VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
new VirtualMachineCustomImageConfig.Builder();
@@ -160,6 +164,7 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mExecutorService = Executors.newCachedThreadPool();
try {
// To ensure that the previous display service is removed.
IVirtualizationServiceInternal.Stub.asInterface(
@@ -239,10 +244,13 @@
if (DEBUG) {
InputStream console = mVirtualMachine.getConsoleOutput();
InputStream log = mVirtualMachine.getLogOutput();
- mExecutorService.execute(new Reader("console", console));
+ OutputStream consoleLogFile =
+ new LineBufferedOutputStream(
+ getApplicationContext().openFileOutput("console.log", 0));
+ mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile));
mExecutorService.execute(new Reader("log", log));
}
- } catch (VirtualMachineException e) {
+ } catch (VirtualMachineException | IOException e) {
throw new RuntimeException(e);
}
@@ -305,6 +313,15 @@
}
@Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mExecutorService != null) {
+ mExecutorService.shutdownNow();
+ }
+ Log.d(TAG, "destroyed");
+ }
+
+ @Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
@@ -359,4 +376,49 @@
}
}
}
+
+ private static class CopyStreamTask implements Runnable {
+ private final String mName;
+ private final InputStream mIn;
+ private final OutputStream mOut;
+
+ CopyStreamTask(String name, InputStream in, OutputStream out) {
+ mName = name;
+ mIn = in;
+ mOut = out;
+ }
+
+ @Override
+ public void run() {
+ try {
+ byte[] buffer = new byte[2048];
+ while (!Thread.interrupted()) {
+ int len = mIn.read(buffer);
+ if (len < 0) {
+ break;
+ }
+ mOut.write(buffer, 0, len);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while posting " + mName, e);
+ }
+ }
+ }
+
+ private static class LineBufferedOutputStream extends BufferedOutputStream {
+ LineBufferedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len) throws IOException {
+ super.write(buf, off, len);
+ for (int i = 0; i < len; ++i) {
+ if (buf[off + i] == '\n') {
+ flush();
+ break;
+ }
+ }
+ }
+ }
}