Merge "Add the onPayloadStarted callback API"
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index baf0242..b6c7714 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -18,7 +18,9 @@
import android.app.Application;
import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineException;
import android.system.virtualmachine.VirtualMachineManager;
@@ -36,6 +38,7 @@
import androidx.lifecycle.ViewModelProvider;
import java.io.BufferedReader;
+import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
@@ -52,10 +55,12 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView consoleView = (TextView) findViewById(R.id.consoleOutput);
+ TextView payloadView = (TextView) findViewById(R.id.payloadOutput);
Button runStopButton = (Button) findViewById(R.id.runStopButton);
- ScrollView scrollView = (ScrollView) findViewById(R.id.scrollview);
+ ScrollView scrollView = (ScrollView) findViewById(R.id.scrollConsoleOutput);
- // When the console model is updated, append the new line to the text view.
+ // When the console output or payload output is updated, append the new line to the
+ // corresponding text view.
VirtualMachineModel model = new ViewModelProvider(this).get(VirtualMachineModel.class);
model.getConsoleOutput()
.observeForever(
@@ -66,6 +71,14 @@
scrollView.fullScroll(View.FOCUS_DOWN);
}
});
+ model.getPayloadOutput()
+ .observeForever(
+ new Observer<String>() {
+ @Override
+ public void onChanged(String line) {
+ payloadView.append(line + "\n");
+ }
+ });
// When the VM status is updated, change the label of the button
model.getStatus()
@@ -75,9 +88,10 @@
public void onChanged(VirtualMachine.Status status) {
if (status == VirtualMachine.Status.RUNNING) {
runStopButton.setText("Stop");
+ consoleView.setText("");
+ payloadView.setText("");
} else {
runStopButton.setText("Run");
- consoleView.setText("");
}
}
});
@@ -101,6 +115,7 @@
public static class VirtualMachineModel extends AndroidViewModel {
private VirtualMachine mVirtualMachine;
private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
+ private final MutableLiveData<String> mPayloadOutput = new MutableLiveData<>();
private final MutableLiveData<VirtualMachine.Status> mStatus = new MutableLiveData<>();
public VirtualMachineModel(Application app) {
@@ -121,6 +136,31 @@
VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
mVirtualMachine = vmm.getOrCreate("demo_vm", config);
mVirtualMachine.run();
+ mVirtualMachine.setCallback(
+ new VirtualMachineCallback() {
+ @Override
+ public void onPayloadStarted(
+ VirtualMachine vm, ParcelFileDescriptor out) {
+ try {
+ BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(
+ new FileInputStream(
+ out.getFileDescriptor())));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ mPayloadOutput.postValue(line);
+ }
+ } catch (IOException e) {
+ // Consume
+ }
+ }
+
+ @Override
+ public void onDied(VirtualMachine vm) {
+ mStatus.postValue(VirtualMachine.Status.STOPPED);
+ }
+ });
mStatus.postValue(mVirtualMachine.getStatus());
} catch (VirtualMachineException e) {
throw new RuntimeException(e);
@@ -164,6 +204,11 @@
return mConsoleOutput;
}
+ /** Returns the payload output from the VM */
+ public LiveData<String> getPayloadOutput() {
+ return mPayloadOutput;
+ }
+
/** Returns the status of the VM */
public LiveData<VirtualMachine.Status> getStatus() {
return mStatus;
diff --git a/demo/res/layout/activity_main.xml b/demo/res/layout/activity_main.xml
index cd30f35..e100027 100644
--- a/demo/res/layout/activity_main.xml
+++ b/demo/res/layout/activity_main.xml
@@ -33,10 +33,38 @@
android:text="Debug mode" />
</LinearLayout>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="App output:" />
+
<ScrollView
- android:id="@+id/scrollview"
+ android:id="@+id/scrollPayloadOutput"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <TextView
+ android:id="@+id/payloadOutput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#9089e0"
+ android:fontFamily="monospace"
+ android:textColor="#000000" />
+ </ScrollView>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="Console output:" />
+
+ <ScrollView
+ android:id="@+id/scrollConsoleOutput"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2">
<TextView
android:id="@+id/consoleOutput"
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 53d6864..0e549ae 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -17,10 +17,12 @@
package android.system.virtualmachine;
import android.content.Context;
+import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualMachineCallback;
import android.system.virtualizationservice.IVirtualizationService;
import java.io.File;
@@ -85,6 +87,9 @@
/** Handle to the "running" VM. */
private IVirtualMachine mVirtualMachine;
+ /** The registered callback */
+ private VirtualMachineCallback mCallback;
+
private ParcelFileDescriptor mConsoleReader;
private ParcelFileDescriptor mConsoleWriter;
@@ -186,6 +191,19 @@
}
/**
+ * Registers the callback object to get events from the virtual machine. If a callback was
+ * already registered, it is replaced with the new one.
+ */
+ public void setCallback(VirtualMachineCallback callback) {
+ mCallback = callback;
+ }
+
+ /** Returns the currently registered callback. */
+ public VirtualMachineCallback getCallback() {
+ return mCallback;
+ }
+
+ /**
* Runs this virtual machine. The returning of this method however doesn't mean that the VM has
* actually started running or the OS has booted there. Such events can be notified by
* registering a callback object (not implemented currently).
@@ -208,6 +226,40 @@
android.system.virtualizationservice.VirtualMachineConfig.appConfig(
getConfig().toParcel()),
mConsoleWriter);
+
+ mVirtualMachine.registerCallback(
+ new IVirtualMachineCallback.Stub() {
+ @Override
+ public void onPayloadStarted(int cid, ParcelFileDescriptor stream) {
+ final VirtualMachineCallback cb = mCallback;
+ if (cb == null) {
+ return;
+ }
+ cb.onPayloadStarted(VirtualMachine.this, stream);
+ }
+
+ @Override
+ public void onDied(int cid) {
+ final VirtualMachineCallback cb = mCallback;
+ if (cb == null) {
+ return;
+ }
+ cb.onDied(VirtualMachine.this);
+ }
+ });
+ service.asBinder()
+ .linkToDeath(
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ final VirtualMachineCallback cb = mCallback;
+ if (cb != null) {
+ cb.onDied(VirtualMachine.this);
+ }
+ }
+ },
+ 0);
+
} catch (IOException e) {
throw new VirtualMachineException(e);
} catch (RemoteException e) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
new file mode 100644
index 0000000..0267de8
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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 android.system.virtualmachine;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Callback interface to get notified with the events from the virtual machine. The methods are
+ * executed on a binder thread. Implementations can make blocking calls in the methods.
+ *
+ * @hide
+ */
+public interface VirtualMachineCallback {
+
+ /** Called when the payload starts in the VM. */
+ void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stdout);
+
+ /** Called when the VM died. */
+ void onDied(VirtualMachine vm);
+}
diff --git a/microdroid/sepolicy/system/private/microdroid_app.te b/microdroid/sepolicy/system/private/microdroid_app.te
index eff9120..c8e75a4 100644
--- a/microdroid/sepolicy/system/private/microdroid_app.te
+++ b/microdroid/sepolicy/system/private/microdroid_app.te
@@ -43,3 +43,6 @@
rebind
use
};
+
+# Allow microdroid_app to use vsock inherited from microdroid_manager
+allow microdroid_app microdroid_manager:vsock_socket { read write };
diff --git a/microdroid/sepolicy/system/private/microdroid_manager.te b/microdroid/sepolicy/system/private/microdroid_manager.te
index 53c63ae..781a5e1 100644
--- a/microdroid/sepolicy/system/private/microdroid_manager.te
+++ b/microdroid/sepolicy/system/private/microdroid_manager.te
@@ -29,3 +29,6 @@
allow microdroid_manager fuse:dir r_dir_perms;
allow microdroid_manager fuse:file rx_file_perms;
')
+
+# Let microdroid_manager to create a vsock connection back to the host VM
+allow microdroid_manager self:vsock_socket { create_socket_perms_no_ioctl };
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 267147f..15c439b 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -18,6 +18,7 @@
"libprotobuf",
"libserde",
"libserde_json",
+ "libvsock",
],
init_rc: ["microdroid_manager.rc"],
}
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 9bcfa67..d88ba1a 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -19,12 +19,15 @@
use anyhow::{anyhow, bail, Result};
use keystore2_system_property::PropertyWatcher;
-use log::info;
+use log::{error, info};
use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
-use std::fs;
+use std::fs::{self, File};
+use std::os::unix::io::{FromRawFd, IntoRawFd};
use std::path::Path;
-use std::process::Command;
+use std::process::{Command, Stdio};
+use std::str;
use std::time::Duration;
+use vsock::VsockStream;
const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -38,7 +41,10 @@
// TODO(jooyung): wait until sys.boot_completed?
if let Some(main_task) = &config.task {
- exec_task(main_task)?;
+ exec_task(main_task).map_err(|e| {
+ error!("failed to execute task: {}", e);
+ e
+ })?;
}
}
@@ -51,16 +57,38 @@
Ok(serde_json::from_reader(file)?)
}
+/// Executes the given task. Stdout of the task is piped into the vsock stream to the
+/// virtualizationservice in the host side.
fn exec_task(task: &Task) -> Result<()> {
- info!("executing main task {:?}...", task);
- let exit_status = build_command(task)?.spawn()?.wait()?;
- if exit_status.success() {
- Ok(())
- } else {
- match exit_status.code() {
- Some(code) => bail!("task exited with exit code: {}", code),
- None => bail!("task terminated by signal"),
+ const VMADDR_CID_HOST: u32 = 2;
+ const PORT_VIRT_SVC: u32 = 3000;
+ let stdout = match VsockStream::connect_with_cid_port(VMADDR_CID_HOST, PORT_VIRT_SVC) {
+ Ok(stream) => {
+ // SAFETY: the ownership of the underlying file descriptor is transferred from stream
+ // to the file object, and then into the Command object. When the command is finished,
+ // the file descriptor is closed.
+ let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
+ Stdio::from(f)
}
+ Err(e) => {
+ error!("failed to connect to virtualization service: {}", e);
+ // Don't fail hard here. Even if we failed to connect to the virtualizationservice,
+ // we keep executing the task. This can happen if the owner of the VM doesn't register
+ // callback to accept the stream. Use /dev/null as the stdout so that the task can
+ // make progress without waiting for someone to consume the output.
+ Stdio::null()
+ }
+ };
+ info!("executing main task {:?}...", task);
+ // TODO(jiyong): consider piping the stream into stdio (and probably stderr) as well.
+ let mut child = build_command(task)?.stdout(stdout).spawn()?;
+ match child.wait()?.code() {
+ Some(0) => {
+ info!("task successfully finished");
+ Ok(())
+ }
+ Some(code) => bail!("task exited with exit code: {}", code),
+ None => bail!("task terminated by signal"),
}
}
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 0b8f2e5..40aa139 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -39,6 +39,7 @@
"libuuid",
"libvmconfig",
"libzip",
+ "libvsock",
],
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index e864414..33c9716 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -27,6 +27,9 @@
/**
* Register a Binder object to get callbacks when the state of the VM changes, such as if it
* dies.
+ *
+ * TODO(jiyong): this should be registered when IVirtualizationService.run is called. Otherwise,
+ * we might miss some events that happen before the registration is done.
*/
void registerCallback(IVirtualMachineCallback callback);
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
index 10ef31b..7bb18a4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -23,6 +23,16 @@
*/
oneway interface IVirtualMachineCallback {
/**
+ * Called when the payload starts in the VM. `stdout` is the stdout of the payload.
+ *
+ * <p>Note: when the virtual machine object is shared to multiple processes and they register
+ * this callback to the same virtual machine object, the processes will compete to read from the
+ * same payload stdout. As a result, each process might get only a part of the entire output
+ * stream. To avoid such a case, keep only one process to read from the stdout.
+ */
+ void onPayloadStarted(int cid, in ParcelFileDescriptor stdout);
+
+ /**
* Called when the VM dies.
*
* Note that this will not be called if the VirtualizationService itself dies, so you should
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 8bdfa9d..661abdc 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -36,16 +36,17 @@
};
use anyhow::{bail, Result};
use disk::QcowFile;
-use log::{debug, error, warn};
+use log::{debug, error, warn, info};
use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
use std::convert::TryInto;
use std::ffi::CString;
use std::fs::{File, create_dir};
use std::num::NonZeroU32;
-use std::os::unix::io::AsRawFd;
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, Weak};
use vmconfig::{VmConfig, Partition};
+use vsock::{VsockListener, SockAddr, VsockStream};
use zip::ZipArchive;
pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
@@ -62,10 +63,17 @@
const MICRODROID_REQUIRED_APEXES: [&str; 3] =
["com.android.adbd", "com.android.i18n", "com.android.os.statsd"];
+/// The CID representing the host VM
+const VMADDR_CID_HOST: u32 = 2;
+
+/// Port number that virtualizationservice listens on connections from the guest VMs for the
+/// payload output
+const PORT_VIRT_SERVICE: u32 = 3000;
+
/// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
#[derive(Debug, Default)]
pub struct VirtualizationService {
- state: Mutex<State>,
+ state: Arc<Mutex<State>>,
}
impl Interface for VirtualizationService {}
@@ -235,6 +243,45 @@
}
}
+impl VirtualizationService {
+ pub fn init() -> VirtualizationService {
+ let service = VirtualizationService::default();
+ let state = service.state.clone(); // reference to state (not the state itself) is copied
+ std::thread::spawn(move || {
+ handle_connection_from_vm(state).unwrap();
+ });
+ service
+ }
+}
+
+/// Waits for incoming connections from VM. If a new connection is made, notify the event to the
+/// client via the callback (if registered).
+fn handle_connection_from_vm(state: Arc<Mutex<State>>) -> Result<()> {
+ let listener = VsockListener::bind_with_cid_port(VMADDR_CID_HOST, PORT_VIRT_SERVICE)?;
+ for stream in listener.incoming() {
+ let stream = match stream {
+ Err(e) => {
+ warn!("invalid incoming connection: {}", e);
+ continue;
+ }
+ Ok(s) => s,
+ };
+ if let Ok(SockAddr::Vsock(addr)) = stream.peer_addr() {
+ let cid = addr.cid();
+ let port = addr.port();
+ info!("connected from cid={}, port={}", cid, port);
+ if cid < FIRST_GUEST_CID {
+ warn!("connection is not from a guest VM");
+ continue;
+ }
+ if let Some(vm) = state.lock().unwrap().get_vm(cid) {
+ vm.callbacks.notify_payload_started(cid, stream);
+ }
+ }
+ }
+ Ok(())
+}
+
/// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
///
/// This may involve assembling a composite disk from a set of partition images.
@@ -442,12 +489,25 @@
pub struct VirtualMachineCallbacks(Mutex<Vec<Strong<dyn IVirtualMachineCallback>>>);
impl VirtualMachineCallbacks {
+ /// Call all registered callbacks to notify that the payload has started.
+ pub fn notify_payload_started(&self, cid: Cid, stream: VsockStream) {
+ let callbacks = &*self.0.lock().unwrap();
+ // SAFETY: ownership is transferred from stream to f
+ let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
+ let pfd = ParcelFileDescriptor::new(f);
+ for callback in callbacks {
+ if let Err(e) = callback.onPayloadStarted(cid as i32, &pfd) {
+ error!("Error notifying payload start event from VM CID {}: {}", cid, e);
+ }
+ }
+ }
+
/// Call all registered callbacks to say that the VM has died.
pub fn callback_on_died(&self, cid: Cid) {
let callbacks = &*self.0.lock().unwrap();
for callback in callbacks {
if let Err(e) = callback.onDied(cid as i32) {
- error!("Error calling callback: {}", e);
+ error!("Error notifying exit of VM CID {}: {}", cid, e);
}
}
}
@@ -492,6 +552,11 @@
self.vms.push(vm);
}
+ /// Get a VM that corresponds to the given cid
+ fn get_vm(&self, cid: Cid) -> Option<Arc<VmInstance>> {
+ self.vms().into_iter().find(|vm| vm.cid == cid)
+ }
+
/// Store a strong VM reference.
fn debug_hold_vm(&mut self, vm: Strong<dyn IVirtualMachine>) {
self.debug_held_vms.push(vm);
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 658203b..46ddd2e 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -39,7 +39,7 @@
android_logger::Config::default().with_tag(LOG_TAG).with_min_level(Level::Trace),
);
- let service = VirtualizationService::default();
+ let service = VirtualizationService::init();
let service = BnVirtualizationService::new_binder(
service,
BinderFeatures { set_requesting_sid: true, ..BinderFeatures::default() },
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 01fc724..184a396 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -30,7 +30,7 @@
use android_system_virtualizationservice::binder::{Interface, Result as BinderResult};
use anyhow::{Context, Error};
use std::fs::File;
-use std::io;
+use std::io::{self, BufRead, BufReader};
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::Path;
use vmconfig::VmConfig;
@@ -129,7 +129,7 @@
/// If the returned DeathRecipient is dropped then this will no longer do anything.
fn wait_for_death(binder: &mut impl IBinder, dead: AtomicFlag) -> Result<DeathRecipient, Error> {
let mut death_recipient = DeathRecipient::new(move || {
- println!("VirtualizationService died");
+ eprintln!("VirtualizationService unexpectedly died");
dead.raise();
});
binder.link_to_death(&mut death_recipient)?;
@@ -144,8 +144,26 @@
impl Interface for VirtualMachineCallback {}
impl IVirtualMachineCallback for VirtualMachineCallback {
+ fn onPayloadStarted(&self, _cid: i32, stdout: &ParcelFileDescriptor) -> BinderResult<()> {
+ // Show the stdout of the payload
+ let mut reader = BufReader::new(stdout.as_ref());
+ loop {
+ let mut s = String::new();
+ match reader.read_line(&mut s) {
+ Ok(0) => break,
+ Ok(_) => print!("{}", s),
+ Err(e) => eprintln!("error reading from virtual machine: {}", e),
+ };
+ }
+ Ok(())
+ }
+
fn onDied(&self, _cid: i32) -> BinderResult<()> {
- println!("VM died");
+ // No need to explicitly report the event to the user (e.g. via println!) because this
+ // callback is registered only when the vm tool is invoked as interactive mode (e.g. not
+ // --daemonize) in which case the tool will exit to the shell prompt upon VM shutdown.
+ // Printing something will actually even confuse the user as the output from the app
+ // payload is printed.
self.dead.raise();
Ok(())
}