blob: 5c2ea74522b5b6a08b253e985d28a771e05f2036 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Provides parts of crosvm as a library to communicate with running crosvm instances.
//!
//! This crate is a programmatic alternative to invoking crosvm with subcommands that produce the
//! result on stdout.
//!
//! Downstream projects rely on this library maintaining a stable API surface.
//! Do not make changes to this library without consulting the crosvm externalization team.
//! Email: crosvm-dev@chromium.org
//! For more information see:
//! <https://crosvm.dev/book/running_crosvm/programmatic_interaction.html#usage>
use std::convert::TryFrom;
use std::convert::TryInto;
use std::ffi::CStr;
use std::panic::catch_unwind;
use std::path::Path;
use std::path::PathBuf;
use libc::c_char;
use libc::ssize_t;
use vm_control::client::*;
use vm_control::BalloonControlCommand;
use vm_control::BalloonStats;
use vm_control::BalloonWSS;
use vm_control::DiskControlCommand;
use vm_control::RegisteredEvent;
use vm_control::UsbControlAttachedDevice;
use vm_control::UsbControlResult;
use vm_control::VmRequest;
use vm_control::VmResponse;
use vm_control::WSSBucket;
use vm_control::USB_CONTROL_MAX_PORTS;
fn validate_socket_path(socket_path: *const c_char) -> Option<PathBuf> {
if !socket_path.is_null() {
let socket_path = unsafe { CStr::from_ptr(socket_path) };
Some(PathBuf::from(socket_path.to_str().ok()?))
} else {
None
}
}
/// Stops the crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_stop_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::Exit, socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Suspends the crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_suspend_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::Suspend, socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Resumes the crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_resume_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::Resume, socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Creates an RT vCPU for the crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_make_rt_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::MakeRT, socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Adjusts the balloon size of the crosvm instance whose control socket is
/// listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_balloon_vms(socket_path: *const c_char, num_bytes: u64) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
let command = BalloonControlCommand::Adjust { num_bytes };
vms_request(&VmRequest::BalloonCommand(command), socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Enable vmm swap for crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_swap_enable_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::Swap(SwapCommand::Enable), socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Swap out staging memory for crosvm instance whose control socket is listening
/// on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_swap_swapout_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::Swap(SwapCommand::SwapOut), socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Disable vmm swap for crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_swap_disable_vm(socket_path: *const c_char) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
vms_request(&VmRequest::Swap(SwapCommand::Disable), socket_path).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Represents an individual attached USB device.
#[repr(C)]
pub struct UsbDeviceEntry {
/// Internal port index used for identifying this individual device.
port: u8,
/// USB vendor ID
vendor_id: u16,
/// USB product ID
product_id: u16,
}
impl From<&UsbControlAttachedDevice> for UsbDeviceEntry {
fn from(other: &UsbControlAttachedDevice) -> Self {
Self {
port: other.port,
vendor_id: other.vendor_id,
product_id: other.product_id,
}
}
}
/// Simply returns the maximum possible number of USB devices
#[no_mangle]
pub extern "C" fn crosvm_client_max_usb_devices() -> usize {
USB_CONTROL_MAX_PORTS
}
/// Returns all USB devices passed through the crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns the amount of entries written.
/// # Arguments
///
/// * `socket_path` - Path to the crosvm control socket
/// * `entries` - Pointer to an array of `UsbDeviceEntry` where the details about the attached
/// devices will be written to
/// * `entries_length` - Amount of entries in the array specified by `entries`
///
/// Use the value returned by [`crosvm_client_max_usb_devices()`] to determine the size of the input
/// array to this function.
#[no_mangle]
pub extern "C" fn crosvm_client_usb_list(
socket_path: *const c_char,
entries: *mut UsbDeviceEntry,
entries_length: ssize_t,
) -> ssize_t {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if let Ok(UsbControlResult::Devices(res)) = do_usb_list(&socket_path) {
let mut i = 0;
for entry in res.iter().filter(|x| x.valid()) {
if i >= entries_length {
break;
}
unsafe {
*entries.offset(i) = entry.into();
i += 1;
}
}
i
} else {
-1
}
} else {
-1
}
})
.unwrap_or(-1)
}
/// Attaches an USB device to crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns the amount of entries written.
/// # Arguments
///
/// * `socket_path` - Path to the crosvm control socket
/// * `bus` - USB device bus ID (unused)
/// * `addr` - USB device address (unused)
/// * `vid` - USB device vendor ID (unused)
/// * `pid` - USB device product ID (unused)
/// * `dev_path` - Path to the USB device (Most likely `/dev/bus/usb/<bus>/<addr>`).
/// * `out_port` - (optional) internal port will be written here if provided.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_usb_attach(
socket_path: *const c_char,
_bus: u8,
_addr: u8,
_vid: u16,
_pid: u16,
dev_path: *const c_char,
out_port: *mut u8,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if dev_path.is_null() {
return false;
}
let dev_path = Path::new(unsafe { CStr::from_ptr(dev_path) }.to_str().unwrap_or(""));
if let Ok(UsbControlResult::Ok { port }) = do_usb_attach(socket_path, dev_path) {
if !out_port.is_null() {
unsafe { *out_port = port };
}
true
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}
/// Detaches an USB device from crosvm instance whose control socket is listening on `socket_path`.
/// `port` determines device to be detached.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_usb_detach(socket_path: *const c_char, port: u8) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
do_usb_detach(socket_path, port).is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Modifies the battery status of crosvm instance whose control socket is listening on
/// `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_modify_battery(
socket_path: *const c_char,
battery_type: *const c_char,
property: *const c_char,
target: *const c_char,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if battery_type.is_null() || property.is_null() || target.is_null() {
return false;
}
let battery_type = unsafe { CStr::from_ptr(battery_type) };
let property = unsafe { CStr::from_ptr(property) };
let target = unsafe { CStr::from_ptr(target) };
do_modify_battery(
socket_path,
battery_type.to_str().unwrap(),
property.to_str().unwrap(),
target.to_str().unwrap(),
)
.is_ok()
} else {
false
}
})
.unwrap_or(false)
}
/// Resizes the disk of the crosvm instance whose control socket is listening on `socket_path`.
///
/// The function returns true on success or false if an error occured.
#[no_mangle]
pub extern "C" fn crosvm_client_resize_disk(
socket_path: *const c_char,
disk_index: u64,
new_size: u64,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if let Ok(disk_index) = usize::try_from(disk_index) {
let request = VmRequest::DiskCommand {
disk_index,
command: DiskControlCommand::Resize { new_size },
};
vms_request(&request, socket_path).is_ok()
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}
/// Similar to internally used `BalloonStats` but using i64 instead of
/// Option<u64>. `None` (or values bigger than i64::max) will be encoded as -1.
#[repr(C)]
pub struct BalloonStatsFfi {
swap_in: i64,
swap_out: i64,
major_faults: i64,
minor_faults: i64,
free_memory: i64,
total_memory: i64,
available_memory: i64,
disk_caches: i64,
hugetlb_allocations: i64,
hugetlb_failures: i64,
shared_memory: i64,
unevictable_memory: i64,
}
impl From<&BalloonStats> for BalloonStatsFfi {
fn from(other: &BalloonStats) -> Self {
let convert = |x: Option<u64>| -> i64 { x.and_then(|y| y.try_into().ok()).unwrap_or(-1) };
Self {
swap_in: convert(other.swap_in),
swap_out: convert(other.swap_out),
major_faults: convert(other.major_faults),
minor_faults: convert(other.minor_faults),
free_memory: convert(other.free_memory),
total_memory: convert(other.total_memory),
available_memory: convert(other.available_memory),
disk_caches: convert(other.disk_caches),
hugetlb_allocations: convert(other.hugetlb_allocations),
hugetlb_failures: convert(other.hugetlb_failures),
shared_memory: convert(other.shared_memory),
unevictable_memory: convert(other.unevictable_memory),
}
}
}
/// Returns balloon stats of the crosvm instance whose control socket is listening on `socket_path`.
///
/// The parameters `stats` and `actual` are optional and will only be written to if they are
/// non-null.
///
/// The function returns true on success or false if an error occured.
///
/// # Note
///
/// Entries in `BalloonStatsFfi` that are not available will be set to `-1`.
#[no_mangle]
pub extern "C" fn crosvm_client_balloon_stats(
socket_path: *const c_char,
stats: *mut BalloonStatsFfi,
actual: *mut u64,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
let request = &VmRequest::BalloonCommand(BalloonControlCommand::Stats {});
if let Ok(VmResponse::BalloonStats {
stats: ref balloon_stats,
balloon_actual,
}) = handle_request(request, socket_path)
{
if !stats.is_null() {
unsafe {
*stats = balloon_stats.into();
}
}
if !actual.is_null() {
unsafe {
*actual = balloon_actual;
}
}
true
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}
/// Externally exposed variant of BalloonWss/WSSBucket, used for FFI.
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct WSSBucketFfi {
age: u64,
bytes: [u64; 2],
}
impl WSSBucketFfi {
fn new() -> Self {
Self {
age: 0,
bytes: [0, 0],
}
}
}
impl From<WSSBucket> for WSSBucketFfi {
fn from(other: WSSBucket) -> Self {
Self {
age: other.age,
bytes: other.bytes,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct BalloonWSSFfi {
wss: [WSSBucketFfi; 4],
}
impl From<&BalloonWSS> for BalloonWSSFfi {
fn from(other: &BalloonWSS) -> Self {
let mut ffi = Self {
wss: [WSSBucketFfi::new(); 4],
};
for (ffi_wss, other_wss) in ffi.wss.iter_mut().zip(other.wss) {
*ffi_wss = other_wss.into();
}
ffi
}
}
impl BalloonWSSFfi {
pub fn new() -> Self {
Self {
wss: [WSSBucketFfi::new(); 4],
}
}
}
/// Returns balloon working set size of the crosvm instance whose control socket is listening on socket_path.
#[no_mangle]
pub extern "C" fn crosvm_client_balloon_wss(
socket_path: *const c_char,
wss: *mut BalloonWSSFfi,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
let request = &VmRequest::BalloonCommand(BalloonControlCommand::WorkingSetSize);
if let Ok(VmResponse::BalloonWSS {
wss: ref balloon_wss,
}) = handle_request(request, socket_path)
{
if !wss.is_null() {
// SAFETY: deref of raw pointer is safe because we check to
// make sure the pointer is not null.
unsafe {
*wss = balloon_wss.into();
}
}
true
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}
/// Publically exposed version of RegisteredEvent enum, implemented as an
/// integral newtype for FFI safety.
#[repr(C)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct RegisteredEventFfi(u32);
pub const REGISTERED_EVENT_VIRTIO_BALLOON_WSS_REPORT: RegisteredEventFfi = RegisteredEventFfi(0);
pub const REGISTERED_EVENT_VIRTIO_BALLOON_RESIZE: RegisteredEventFfi = RegisteredEventFfi(1);
pub const REGISTERED_EVENT_VIRTIO_BALLOON_OOM_DEFLATION: RegisteredEventFfi = RegisteredEventFfi(2);
impl TryFrom<RegisteredEventFfi> for RegisteredEvent {
type Error = &'static str;
fn try_from(value: RegisteredEventFfi) -> Result<Self, Self::Error> {
match value.0 {
0 => Ok(RegisteredEvent::VirtioBalloonWssReport),
1 => Ok(RegisteredEvent::VirtioBalloonResize),
2 => Ok(RegisteredEvent::VirtioBalloonOOMDeflation),
_ => Err("RegisteredEventFFi outside of known RegisteredEvent enum range"),
}
}
}
/// Registers the connected process as a listener for `event`.
#[no_mangle]
pub extern "C" fn crosvm_client_register_events_listener(
socket_path: *const c_char,
listening_socket_path: *const c_char,
event: RegisteredEventFfi,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if let Some(listening_socket_path) = validate_socket_path(listening_socket_path) {
if let Ok(event) = event.try_into() {
let request = VmRequest::RegisterListener {
event,
socket_addr: listening_socket_path.to_str().unwrap().to_string(),
};
vms_request(&request, socket_path).is_ok()
} else {
false
}
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}
/// Unegisters the connected process as a listener for `event`.
#[no_mangle]
pub extern "C" fn crosvm_client_unregister_events_listener(
socket_path: *const c_char,
listening_socket_path: *const c_char,
event: RegisteredEventFfi,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if let Some(listening_socket_path) = validate_socket_path(listening_socket_path) {
if let Ok(event) = event.try_into() {
let request = VmRequest::UnregisterListener {
event,
socket_addr: listening_socket_path.to_str().unwrap().to_string(),
};
vms_request(&request, socket_path).is_ok()
} else {
false
}
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}
/// Unegisters the connected process as a listener for all events.
#[no_mangle]
pub extern "C" fn crosvm_client_unregister_listener(
socket_path: *const c_char,
listening_socket_path: *const c_char,
) -> bool {
catch_unwind(|| {
if let Some(socket_path) = validate_socket_path(socket_path) {
if let Some(listening_socket_path) = validate_socket_path(listening_socket_path) {
let request = VmRequest::Unregister {
socket_addr: listening_socket_path.to_str().unwrap().to_string(),
};
vms_request(&request, socket_path).is_ok()
} else {
false
}
} else {
false
}
})
.unwrap_or(false)
}