Add `vm console` command to connect to serial console

`vm console` automatically connects to the first available VM.
`vm console CID` connects to the specified VM.

* Must also pass the `-t` flag to adb-shell to ensure adbd allocates a
  tty.

Bug: 335362012
Test: Launch FC and connect to serial console
  adb shell -t /apex/com.android.virt/bin/vm console
Change-Id: If5f1537d8994593ab7fa026bf98986c6a8c83cb5
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 43f3db0..b6f811e 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -1214,6 +1214,9 @@
                         service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+                if (mConnectVmConsole) {
+                    mVirtualMachine.setHostConsoleName(getHostConsoleName());
+                }
                 mVirtualMachine.start();
             } catch (IOException e) {
                 throw new VirtualMachineException("failed to persist files", e);
@@ -1335,7 +1338,7 @@
      * @hide
      */
     @NonNull
-    public String getHostConsoleName() throws VirtualMachineException {
+    private String getHostConsoleName() throws VirtualMachineException {
         if (!mConnectVmConsole) {
             throw new VirtualMachineException("Host console is not enabled");
         }
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 8e1b6bb..3edefcb 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -1229,6 +1229,10 @@
             .or_service_specific_exception(-1)?;
         Ok(vsock_stream_to_pfd(stream))
     }
+
+    fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
+        self.instance.vm_context.global_context.setHostConsoleName(ptsname)
+    }
 }
 
 impl Drop for VirtualMachine {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 7769f61..637fe31 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -283,7 +283,7 @@
 #[derive(Debug)]
 pub struct VmContext {
     #[allow(dead_code)] // Keeps the global context alive
-    global_context: Strong<dyn IGlobalVmContext>,
+    pub(crate) global_context: Strong<dyn IGlobalVmContext>,
     #[allow(dead_code)] // Keeps the server alive
     vm_server: RpcServer,
 }
@@ -302,7 +302,7 @@
     pub vm_state: Mutex<VmState>,
     /// Global resources allocated for this VM.
     #[allow(dead_code)] // Keeps the context alive
-    vm_context: VmContext,
+    pub(crate) vm_context: VmContext,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// Path to crosvm control socket
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index d76b586..d4001c8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -47,4 +47,7 @@
 
     /** Open a vsock connection to the CID of the VM on the given port. */
     ParcelFileDescriptor connectVsock(int port);
+
+    /** Set the name of the peer end (ptsname) of the host console. */
+    void setHostConsoleName(in @utf8InCpp String pathname);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index 870a342..9f033b1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -33,4 +33,7 @@
      * the PID may have been reused for a different process, so this should not be trusted.
      */
     int requesterPid;
+
+    /** The peer end (ptsname) of the host console. */
+    @nullable @utf8InCpp String hostConsoleName;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index a4d5d19..ea52591 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -21,4 +21,7 @@
 
     /** Get the path to the temporary folder of the VM. */
     String getTemporaryDirectory();
+
+    /** Set the name of the peer end (ptsname) of the host console. */
+    void setHostConsoleName(@utf8InCpp String pathname);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index f69cad4..0b11416 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -287,11 +287,15 @@
             .held_contexts
             .iter()
             .filter_map(|(_, inst)| Weak::upgrade(inst))
-            .map(|vm| VirtualMachineDebugInfo {
-                cid: vm.cid as i32,
-                temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
-                requesterUid: vm.requester_uid as i32,
-                requesterPid: vm.requester_debug_pid,
+            .map(|vm| {
+                let vm = vm.lock().unwrap();
+                VirtualMachineDebugInfo {
+                    cid: vm.cid as i32,
+                    temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
+                    requesterUid: vm.requester_uid as i32,
+                    requesterPid: vm.requester_debug_pid,
+                    hostConsoleName: vm.host_console_name.clone(),
+                }
             })
             .collect();
         Ok(cids)
@@ -660,6 +664,8 @@
     requester_uid: uid_t,
     /// PID of the client who requested this VM instance.
     requester_debug_pid: pid_t,
+    /// Name of the host console.
+    host_console_name: Option<String>,
 }
 
 impl GlobalVmInstance {
@@ -674,7 +680,7 @@
 struct GlobalState {
     /// VM contexts currently allocated to running VMs. A CID is never recycled as long
     /// as there is a strong reference held by a GlobalVmContext.
-    held_contexts: HashMap<Cid, Weak<GlobalVmInstance>>,
+    held_contexts: HashMap<Cid, Weak<Mutex<GlobalVmInstance>>>,
 
     /// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
     dtbo_file: Mutex<Option<File>>,
@@ -754,8 +760,13 @@
         self.held_contexts.retain(|_, instance| instance.strong_count() > 0);
 
         let cid = self.get_next_available_cid()?;
-        let instance = Arc::new(GlobalVmInstance { cid, requester_uid, requester_debug_pid });
-        create_temporary_directory(&instance.get_temp_dir(), Some(requester_uid))?;
+        let instance = Arc::new(Mutex::new(GlobalVmInstance {
+            cid,
+            requester_uid,
+            requester_debug_pid,
+            ..Default::default()
+        }));
+        create_temporary_directory(&instance.lock().unwrap().get_temp_dir(), Some(requester_uid))?;
 
         self.held_contexts.insert(cid, Arc::downgrade(&instance));
         let binder = GlobalVmContext { instance, ..Default::default() };
@@ -835,7 +846,7 @@
 #[derive(Debug, Default)]
 struct GlobalVmContext {
     /// Strong reference to the context's instance data structure.
-    instance: Arc<GlobalVmInstance>,
+    instance: Arc<Mutex<GlobalVmInstance>>,
     /// Keeps our service process running as long as this VM context exists.
     #[allow(dead_code)]
     lazy_service_guard: LazyServiceGuard,
@@ -845,11 +856,16 @@
 
 impl IGlobalVmContext for GlobalVmContext {
     fn getCid(&self) -> binder::Result<i32> {
-        Ok(self.instance.cid as i32)
+        Ok(self.instance.lock().unwrap().cid as i32)
     }
 
     fn getTemporaryDirectory(&self) -> binder::Result<String> {
-        Ok(self.instance.get_temp_dir().to_string_lossy().to_string())
+        Ok(self.instance.lock().unwrap().get_temp_dir().to_string_lossy().to_string())
+    }
+
+    fn setHostConsoleName(&self, pathname: &str) -> binder::Result<()> {
+        self.instance.lock().unwrap().host_console_name = Some(pathname.to_string());
+        Ok(())
     }
 }
 
diff --git a/vm/src/main.rs b/vm/src/main.rs
index a250c35..3c0887c 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -24,15 +24,18 @@
 };
 #[cfg(not(llpvm_changes))]
 use anyhow::anyhow;
-use anyhow::{Context, Error};
+use anyhow::{bail, Context, Error};
 use binder::{ProcessState, Strong};
 use clap::{Args, Parser};
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app, command_run_microdroid};
 use serde::Serialize;
+use std::io::{self, IsTerminal};
 use std::num::NonZeroU16;
+use std::os::unix::process::CommandExt;
 use std::path::{Path, PathBuf};
+use std::process::Command;
 
 #[derive(Args, Default)]
 /// Collection of flags that are at VM level and therefore applicable to all subcommands
@@ -324,6 +327,11 @@
         /// Path to idsig of the APK
         path: PathBuf,
     },
+    /// Connect to the serial console of a VM
+    Console {
+        /// CID of the VM
+        cid: Option<i32>,
+    },
 }
 
 fn parse_debug_level(s: &str) -> Result<DebugLevel, String> {
@@ -386,6 +394,7 @@
         Opt::CreateIdsig { apk, path } => {
             command_create_idsig(get_service()?.as_ref(), &apk, &path)
         }
+        Opt::Console { cid } => command_console(cid),
     }
 }
 
@@ -450,6 +459,21 @@
     Ok(())
 }
 
+fn command_console(cid: Option<i32>) -> Result<(), Error> {
+    if !io::stdin().is_terminal() {
+        bail!("Stdin must be a terminal (tty). Use 'adb shell -t' to force allocate tty.");
+    }
+    let mut vms = get_service()?.debugListVms().context("Failed to get list of VMs")?;
+    if let Some(cid) = cid {
+        vms.retain(|vm_info| vm_info.cid == cid);
+    }
+    let host_console_name = vms
+        .into_iter()
+        .find_map(|vm_info| vm_info.hostConsoleName)
+        .context("Failed to get VM with console")?;
+    Err(Command::new("microcom").arg(host_console_name).exec().into())
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;