Add FFI library providing control socket access

This allows other languages to communicate directly with the control
socket without having to invoke `crosvm`

BUG=None
TEST=Ran ./run_tests

Change-Id: Icbf5905c41643b080bae3613b73a032467db1c4c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2772798
Tested-by: kokoro <noreply+kokoro@google.com>
Commit-Queue: Kevin Hamacher <hamacher@google.com>
Commit-Queue: Daniel Verkamp <dverkamp@chromium.org>
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
diff --git a/Cargo.lock b/Cargo.lock
index 4d63985..725b837 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -587,6 +587,7 @@
  "libc",
  "log",
  "protobuf",
+ "sys_util",
  "thiserror",
  "zeroize",
 ]
@@ -603,6 +604,15 @@
 ]
 
 [[package]]
+name = "libcrosvm_control"
+version = "0.1.0"
+dependencies = [
+ "base",
+ "libc",
+ "vm_control",
+]
+
+[[package]]
 name = "libdbus-sys"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index d068019..3b2f0cf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@
     "fuzz",
     "qcow_utils",
     "integration_tests",
+    "libcrosvm_control",
 ]
 exclude = [
     "assertions",
diff --git a/libcrosvm_control/Cargo.toml b/libcrosvm_control/Cargo.toml
new file mode 100644
index 0000000..2ed68b8
--- /dev/null
+++ b/libcrosvm_control/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "libcrosvm_control"
+version = "0.1.0"
+authors = ["The Chromium OS Authors"]
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+base = { path = "../base" }
+vm_control = { path = "../vm_control" }
+libc = "0.2.65"
diff --git a/libcrosvm_control/src/lib.rs b/libcrosvm_control/src/lib.rs
new file mode 100644
index 0000000..973d64e
--- /dev/null
+++ b/libcrosvm_control/src/lib.rs
@@ -0,0 +1,358 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// 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.
+// Usually you would need to invoke crosvm with subcommands and you'd get the result on
+// stdout.
+use std::convert::{TryFrom, TryInto};
+use std::ffi::CStr;
+use std::panic::catch_unwind;
+use std::path::{Path, PathBuf};
+
+use libc::{c_char, ssize_t};
+
+use vm_control::{
+    client::*, BalloonControlCommand, BalloonStats, DiskControlCommand, UsbControlAttachedDevice,
+    UsbControlResult, VmRequest, VmResponse,
+};
+
+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)
+}
+
+/// 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)
+}
+
+/// 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,
+        }
+    }
+}
+
+/// 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`
+///
+/// Crosvm supports passing through up to 255 devices, so pasing an array with 255 entries will
+/// guarantee to return all entries.
+#[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
+/// * `addr` - USB device address
+/// * `vid` - USB device vendor ID
+/// * `pid` - USB device product ID
+/// * `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, bus, addr, vid, pid, 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,
+}
+
+impl From<&BalloonStats> for BalloonStatsFfi {
+    fn from(other: &BalloonStats) -> Self {
+        let convert =
+            |x: Option<u64>| -> i64 { x.map(|y| y.try_into().ok()).flatten().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),
+        }
+    }
+}
+
+/// 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)
+}
diff --git a/run_tests b/run_tests
index 89b71ca..aa705b5 100755
--- a/run_tests
+++ b/run_tests
@@ -45,6 +45,7 @@
     "kernel_loader": [Requirements.PRIVILEGED],
     "kvm_sys": [Requirements.PRIVILEGED],
     "kvm": [Requirements.PRIVILEGED, Requirements.X86_64],
+    "libcrosvm_control": [],
     "linux_input_sys": [],
     "net_sys": [],
     "net_util": [Requirements.PRIVILEGED],
diff --git a/src/main.rs b/src/main.rs
index 3d166e8..ab3786e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,10 +8,8 @@
 
 use std::collections::BTreeMap;
 use std::default::Default;
-use std::fmt;
 use std::fs::{File, OpenOptions};
 use std::io::{BufRead, BufReader};
-use std::num::ParseIntError;
 use std::path::{Path, PathBuf};
 use std::str::FromStr;
 use std::string::String;
@@ -22,10 +20,7 @@
     set_default_serial_parameters, Pstore, SerialHardware, SerialParameters, SerialType,
     VcpuAffinity,
 };
-use base::{
-    debug, error, getpid, info, kill_process_group, net::UnixSeqpacket, reap_child, syslog,
-    validate_raw_descriptor, warn, FromRawDescriptor, RawDescriptor, Tube,
-};
+use base::{debug, error, getpid, info, kill_process_group, reap_child, syslog, warn};
 #[cfg(feature = "direct")]
 use crosvm::DirectIoOption;
 use crosvm::{
@@ -40,8 +35,11 @@
 use devices::{Ac97Backend, Ac97Parameters};
 use disk::QcowFile;
 use vm_control::{
-    BalloonControlCommand, BatControlCommand, BatControlResult, BatteryType, DiskControlCommand,
-    UsbControlCommand, UsbControlResult, VmRequest, VmResponse, USB_CONTROL_MAX_PORTS,
+    client::{
+        do_modify_battery, do_usb_attach, do_usb_detach, do_usb_list, handle_request, vms_request,
+        ModifyUsbError, ModifyUsbResult,
+    },
+    BalloonControlCommand, BatteryType, DiskControlCommand, UsbControlResult, VmRequest,
 };
 
 fn executable_is_plugin(executable: &Option<Executable>) -> bool {
@@ -1993,76 +1991,37 @@
     }
 }
 
-fn handle_request(
-    request: &VmRequest,
-    args: std::env::Args,
-) -> std::result::Result<VmResponse, ()> {
-    let mut return_result = Err(());
-    for socket_path in args {
-        match UnixSeqpacket::connect(&socket_path) {
-            Ok(s) => {
-                let socket = Tube::new(s);
-                if let Err(e) = socket.send(request) {
-                    error!(
-                        "failed to send request to socket at '{}': {}",
-                        socket_path, e
-                    );
-                    return_result = Err(());
-                    continue;
-                }
-                match socket.recv() {
-                    Ok(response) => return_result = Ok(response),
-                    Err(e) => {
-                        error!(
-                            "failed to send request to socket at2 '{}': {}",
-                            socket_path, e
-                        );
-                        return_result = Err(());
-                        continue;
-                    }
-                }
-            }
-            Err(e) => {
-                error!("failed to connect to socket at '{}': {}", socket_path, e);
-                return_result = Err(());
-            }
-        }
-    }
-
-    return_result
-}
-
-fn vms_request(request: &VmRequest, args: std::env::Args) -> std::result::Result<(), ()> {
-    let response = handle_request(request, args)?;
-    info!("request response was {}", response);
-    Ok(())
-}
-
-fn stop_vms(args: std::env::Args) -> std::result::Result<(), ()> {
+fn stop_vms(mut args: std::env::Args) -> std::result::Result<(), ()> {
     if args.len() == 0 {
         print_help("crosvm stop", "VM_SOCKET...", &[]);
         println!("Stops the crosvm instance listening on each `VM_SOCKET` given.");
         return Err(());
     }
-    vms_request(&VmRequest::Exit, args)
+    let socket_path = &args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
+    vms_request(&VmRequest::Exit, socket_path)
 }
 
-fn suspend_vms(args: std::env::Args) -> std::result::Result<(), ()> {
+fn suspend_vms(mut args: std::env::Args) -> std::result::Result<(), ()> {
     if args.len() == 0 {
         print_help("crosvm suspend", "VM_SOCKET...", &[]);
         println!("Suspends the crosvm instance listening on each `VM_SOCKET` given.");
         return Err(());
     }
-    vms_request(&VmRequest::Suspend, args)
+    let socket_path = &args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
+    vms_request(&VmRequest::Suspend, socket_path)
 }
 
-fn resume_vms(args: std::env::Args) -> std::result::Result<(), ()> {
+fn resume_vms(mut args: std::env::Args) -> std::result::Result<(), ()> {
     if args.len() == 0 {
         print_help("crosvm resume", "VM_SOCKET...", &[]);
         println!("Resumes the crosvm instance listening on each `VM_SOCKET` given.");
         return Err(());
     }
-    vms_request(&VmRequest::Resume, args)
+    let socket_path = &args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
+    vms_request(&VmRequest::Resume, socket_path)
 }
 
 fn balloon_vms(mut args: std::env::Args) -> std::result::Result<(), ()> {
@@ -2080,10 +2039,12 @@
     };
 
     let command = BalloonControlCommand::Adjust { num_bytes };
-    vms_request(&VmRequest::BalloonCommand(command), args)
+    let socket_path = &args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
+    vms_request(&VmRequest::BalloonCommand(command), socket_path)
 }
 
-fn balloon_stats(args: std::env::Args) -> std::result::Result<(), ()> {
+fn balloon_stats(mut args: std::env::Args) -> std::result::Result<(), ()> {
     if args.len() != 1 {
         print_help("crosvm balloon_stats", "VM_SOCKET", &[]);
         println!("Prints virtio balloon statistics for a `VM_SOCKET`.");
@@ -2091,7 +2052,9 @@
     }
     let command = BalloonControlCommand::Stats {};
     let request = &VmRequest::BalloonCommand(command);
-    let response = handle_request(request, args)?;
+    let socket_path = &args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
+    let response = handle_request(request, socket_path)?;
     println!("{}", response);
     Ok(())
 }
@@ -2214,47 +2177,11 @@
         }
     };
 
-    vms_request(&request, args)
+    let socket_path = &args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
+    vms_request(&request, socket_path)
 }
 
-enum ModifyUsbError {
-    ArgMissing(&'static str),
-    ArgParse(&'static str, String),
-    ArgParseInt(&'static str, String, ParseIntError),
-    FailedDescriptorValidate(base::Error),
-    PathDoesNotExist(PathBuf),
-    SocketFailed,
-    UnexpectedResponse(VmResponse),
-    UnknownCommand(String),
-    UsbControl(UsbControlResult),
-}
-
-impl fmt::Display for ModifyUsbError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use self::ModifyUsbError::*;
-
-        match self {
-            ArgMissing(a) => write!(f, "argument missing: {}", a),
-            ArgParse(name, value) => {
-                write!(f, "failed to parse argument {} value `{}`", name, value)
-            }
-            ArgParseInt(name, value, e) => write!(
-                f,
-                "failed to parse integer argument {} value `{}`: {}",
-                name, value, e
-            ),
-            FailedDescriptorValidate(e) => write!(f, "failed to validate file descriptor: {}", e),
-            PathDoesNotExist(p) => write!(f, "path `{}` does not exist", p.display()),
-            SocketFailed => write!(f, "socket failed"),
-            UnexpectedResponse(r) => write!(f, "unexpected response: {}", r),
-            UnknownCommand(c) => write!(f, "unknown command: `{}`", c),
-            UsbControl(e) => write!(f, "{}", e),
-        }
-    }
-}
-
-type ModifyUsbResult<T> = std::result::Result<T, ModifyUsbError>;
-
 fn parse_bus_id_addr(v: &str) -> ModifyUsbResult<(u8, u8, u16, u16)> {
     debug!("parse_bus_id_addr: {}", v);
     let mut ids = v.split(':');
@@ -2279,27 +2206,6 @@
     }
 }
 
-fn raw_descriptor_from_path(path: &Path) -> ModifyUsbResult<RawDescriptor> {
-    if !path.exists() {
-        return Err(ModifyUsbError::PathDoesNotExist(path.to_owned()));
-    }
-    let raw_descriptor = path
-        .file_name()
-        .and_then(|fd_osstr| fd_osstr.to_str())
-        .map_or(
-            Err(ModifyUsbError::ArgParse(
-                "USB_DEVICE_PATH",
-                path.to_string_lossy().into_owned(),
-            )),
-            |fd_str| {
-                fd_str.parse::<libc::c_int>().map_err(|e| {
-                    ModifyUsbError::ArgParseInt("USB_DEVICE_PATH", fd_str.to_owned(), e)
-                })
-            },
-        )?;
-    validate_raw_descriptor(raw_descriptor).map_err(ModifyUsbError::FailedDescriptorValidate)
-}
-
 fn usb_attach(mut args: std::env::Args) -> ModifyUsbResult<UsbControlResult> {
     let val = args
         .next()
@@ -2309,30 +2215,13 @@
         args.next()
             .ok_or(ModifyUsbError::ArgMissing("usb device path"))?,
     );
-    let usb_file = if dev_path.parent() == Some(Path::new("/proc/self/fd")) {
-        // Special case '/proc/self/fd/*' paths. The FD is already open, just use it.
-        // Safe because we will validate |raw_fd|.
-        unsafe { File::from_raw_descriptor(raw_descriptor_from_path(&dev_path)?) }
-    } else {
-        OpenOptions::new()
-            .read(true)
-            .write(true)
-            .open(&dev_path)
-            .map_err(|_| ModifyUsbError::UsbControl(UsbControlResult::FailedToOpenDevice))?
-    };
 
-    let request = VmRequest::UsbCommand(UsbControlCommand::AttachDevice {
-        bus,
-        addr,
-        vid,
-        pid,
-        file: usb_file,
-    });
-    let response = handle_request(&request, args).map_err(|_| ModifyUsbError::SocketFailed)?;
-    match response {
-        VmResponse::UsbResponse(usb_resp) => Ok(usb_resp),
-        r => Err(ModifyUsbError::UnexpectedResponse(r)),
-    }
+    let socket_path = args
+        .next()
+        .ok_or(ModifyUsbError::ArgMissing("control socket path"))?;
+    let socket_path = Path::new(&socket_path);
+
+    do_usb_attach(&socket_path, bus, addr, vid, pid, &dev_path)
 }
 
 fn usb_detach(mut args: std::env::Args) -> ModifyUsbResult<UsbControlResult> {
@@ -2342,25 +2231,19 @@
             p.parse::<u8>()
                 .map_err(|e| ModifyUsbError::ArgParseInt("PORT", p.to_owned(), e))
         })?;
-    let request = VmRequest::UsbCommand(UsbControlCommand::DetachDevice { port });
-    let response = handle_request(&request, args).map_err(|_| ModifyUsbError::SocketFailed)?;
-    match response {
-        VmResponse::UsbResponse(usb_resp) => Ok(usb_resp),
-        r => Err(ModifyUsbError::UnexpectedResponse(r)),
-    }
+    let socket_path = args
+        .next()
+        .ok_or(ModifyUsbError::ArgMissing("control socket path"))?;
+    let socket_path = Path::new(&socket_path);
+    do_usb_detach(&socket_path, port)
 }
 
-fn usb_list(args: std::env::Args) -> ModifyUsbResult<UsbControlResult> {
-    let mut ports: [u8; USB_CONTROL_MAX_PORTS] = Default::default();
-    for (index, port) in ports.iter_mut().enumerate() {
-        *port = index as u8
-    }
-    let request = VmRequest::UsbCommand(UsbControlCommand::ListDevice { ports });
-    let response = handle_request(&request, args).map_err(|_| ModifyUsbError::SocketFailed)?;
-    match response {
-        VmResponse::UsbResponse(usb_resp) => Ok(usb_resp),
-        r => Err(ModifyUsbError::UnexpectedResponse(r)),
-    }
+fn usb_list(mut args: std::env::Args) -> ModifyUsbResult<UsbControlResult> {
+    let socket_path = args
+        .next()
+        .ok_or(ModifyUsbError::ArgMissing("control socket path"))?;
+    let socket_path = Path::new(&socket_path);
+    do_usb_list(&socket_path)
 }
 
 fn modify_usb(mut args: std::env::Args) -> std::result::Result<(), ()> {
@@ -2371,7 +2254,7 @@
     }
 
     // This unwrap will not panic because of the above length check.
-    let command = args.next().unwrap();
+    let command = &args.next().unwrap();
     let result = match command.as_ref() {
         "attach" => usb_attach(args),
         "detach" => usb_detach(args),
@@ -2414,20 +2297,6 @@
     Ok(())
 }
 
-enum ModifyBatError {
-    BatControlErr(BatControlResult),
-}
-
-impl fmt::Display for ModifyBatError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use self::ModifyBatError::*;
-
-        match self {
-            BatControlErr(e) => write!(f, "{}", e),
-        }
-    }
-}
-
 fn modify_battery(mut args: std::env::Args) -> std::result::Result<(), ()> {
     if args.len() < 4 {
         print_help("crosvm battery BATTERY_TYPE ",
@@ -2440,27 +2309,10 @@
     let property = args.next().unwrap();
     let target = args.next().unwrap();
 
-    let response = match battery_type.parse::<BatteryType>() {
-        Ok(type_) => match BatControlCommand::new(property, target) {
-            Ok(cmd) => {
-                let request = VmRequest::BatCommand(type_, cmd);
-                Ok(handle_request(&request, args)?)
-            }
-            Err(e) => Err(ModifyBatError::BatControlErr(e)),
-        },
-        Err(e) => Err(ModifyBatError::BatControlErr(e)),
-    };
+    let socket_path = args.next().unwrap();
+    let socket_path = Path::new(&socket_path);
 
-    match response {
-        Ok(response) => {
-            println!("{}", response);
-            Ok(())
-        }
-        Err(e) => {
-            println!("error {}", e);
-            Err(())
-        }
-    }
+    do_modify_battery(&socket_path, &*battery_type, &*property, &*target)
 }
 
 fn crosvm_main() -> std::result::Result<(), ()> {
diff --git a/vm_control/src/client.rs b/vm_control/src/client.rs
new file mode 100644
index 0000000..3fff3e3
--- /dev/null
+++ b/vm_control/src/client.rs
@@ -0,0 +1,211 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+use crate::*;
+use base::{info, net::UnixSeqpacket, validate_raw_descriptor, RawDescriptor, Tube};
+
+use std::fs::OpenOptions;
+use std::num::ParseIntError;
+use std::path::{Path, PathBuf};
+
+enum ModifyBatError {
+    BatControlErr(BatControlResult),
+}
+
+impl fmt::Display for ModifyBatError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::ModifyBatError::*;
+
+        match self {
+            BatControlErr(e) => write!(f, "{}", e),
+        }
+    }
+}
+
+pub enum ModifyUsbError {
+    ArgMissing(&'static str),
+    ArgParse(&'static str, String),
+    ArgParseInt(&'static str, String, ParseIntError),
+    FailedDescriptorValidate(base::Error),
+    PathDoesNotExist(PathBuf),
+    SocketFailed,
+    UnexpectedResponse(VmResponse),
+    UnknownCommand(String),
+    UsbControl(UsbControlResult),
+}
+
+impl std::fmt::Display for ModifyUsbError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        use self::ModifyUsbError::*;
+
+        match self {
+            ArgMissing(a) => write!(f, "argument missing: {}", a),
+            ArgParse(name, value) => {
+                write!(f, "failed to parse argument {} value `{}`", name, value)
+            }
+            ArgParseInt(name, value, e) => write!(
+                f,
+                "failed to parse integer argument {} value `{}`: {}",
+                name, value, e
+            ),
+            FailedDescriptorValidate(e) => write!(f, "failed to validate file descriptor: {}", e),
+            PathDoesNotExist(p) => write!(f, "path `{}` does not exist", p.display()),
+            SocketFailed => write!(f, "socket failed"),
+            UnexpectedResponse(r) => write!(f, "unexpected response: {}", r),
+            UnknownCommand(c) => write!(f, "unknown command: `{}`", c),
+            UsbControl(e) => write!(f, "{}", e),
+        }
+    }
+}
+
+pub type ModifyUsbResult<T> = std::result::Result<T, ModifyUsbError>;
+
+fn raw_descriptor_from_path(path: &Path) -> ModifyUsbResult<RawDescriptor> {
+    if !path.exists() {
+        return Err(ModifyUsbError::PathDoesNotExist(path.to_owned()));
+    }
+    let raw_descriptor = path
+        .file_name()
+        .and_then(|fd_osstr| fd_osstr.to_str())
+        .map_or(
+            Err(ModifyUsbError::ArgParse(
+                "USB_DEVICE_PATH",
+                path.to_string_lossy().into_owned(),
+            )),
+            |fd_str| {
+                fd_str.parse::<libc::c_int>().map_err(|e| {
+                    ModifyUsbError::ArgParseInt("USB_DEVICE_PATH", fd_str.to_owned(), e)
+                })
+            },
+        )?;
+    validate_raw_descriptor(raw_descriptor).map_err(ModifyUsbError::FailedDescriptorValidate)
+}
+
+pub type VmsRequestResult = std::result::Result<(), ()>;
+
+pub fn vms_request(request: &VmRequest, socket_path: &Path) -> VmsRequestResult {
+    let response = handle_request(request, socket_path)?;
+    info!("request response was {}", response);
+    Ok(())
+}
+
+pub fn do_usb_attach(
+    socket_path: &Path,
+    bus: u8,
+    addr: u8,
+    vid: u16,
+    pid: u16,
+    dev_path: &Path,
+) -> ModifyUsbResult<UsbControlResult> {
+    let usb_file: File = if dev_path.parent() == Some(Path::new("/proc/self/fd")) {
+        // Special case '/proc/self/fd/*' paths. The FD is already open, just use it.
+        // Safe because we will validate |raw_fd|.
+        unsafe { File::from_raw_descriptor(raw_descriptor_from_path(&dev_path)?) }
+    } else {
+        OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open(&dev_path)
+            .map_err(|_| ModifyUsbError::UsbControl(UsbControlResult::FailedToOpenDevice))?
+    };
+
+    let request = VmRequest::UsbCommand(UsbControlCommand::AttachDevice {
+        bus,
+        addr,
+        vid,
+        pid,
+        file: usb_file,
+    });
+    let response =
+        handle_request(&request, socket_path).map_err(|_| ModifyUsbError::SocketFailed)?;
+    match response {
+        VmResponse::UsbResponse(usb_resp) => Ok(usb_resp),
+        r => Err(ModifyUsbError::UnexpectedResponse(r)),
+    }
+}
+
+pub fn do_usb_detach(socket_path: &Path, port: u8) -> ModifyUsbResult<UsbControlResult> {
+    let request = VmRequest::UsbCommand(UsbControlCommand::DetachDevice { port });
+    let response =
+        handle_request(&request, socket_path).map_err(|_| ModifyUsbError::SocketFailed)?;
+    match response {
+        VmResponse::UsbResponse(usb_resp) => Ok(usb_resp),
+        r => Err(ModifyUsbError::UnexpectedResponse(r)),
+    }
+}
+
+pub fn do_usb_list(socket_path: &Path) -> ModifyUsbResult<UsbControlResult> {
+    let mut ports: [u8; USB_CONTROL_MAX_PORTS] = Default::default();
+    for (index, port) in ports.iter_mut().enumerate() {
+        *port = index as u8
+    }
+    let request = VmRequest::UsbCommand(UsbControlCommand::ListDevice { ports });
+    let response =
+        handle_request(&request, socket_path).map_err(|_| ModifyUsbError::SocketFailed)?;
+    match response {
+        VmResponse::UsbResponse(usb_resp) => Ok(usb_resp),
+        r => Err(ModifyUsbError::UnexpectedResponse(r)),
+    }
+}
+
+pub type DoModifyBatteryResult = std::result::Result<(), ()>;
+
+pub fn do_modify_battery(
+    socket_path: &Path,
+    battery_type: &str,
+    property: &str,
+    target: &str,
+) -> DoModifyBatteryResult {
+    let response = match battery_type.parse::<BatteryType>() {
+        Ok(type_) => match BatControlCommand::new(property.to_string(), target.to_string()) {
+            Ok(cmd) => {
+                let request = VmRequest::BatCommand(type_, cmd);
+                Ok(handle_request(&request, socket_path)?)
+            }
+            Err(e) => Err(ModifyBatError::BatControlErr(e)),
+        },
+        Err(e) => Err(ModifyBatError::BatControlErr(e)),
+    };
+
+    match response {
+        Ok(response) => {
+            println!("{}", response);
+            Ok(())
+        }
+        Err(e) => {
+            println!("error {}", e);
+            Err(())
+        }
+    }
+}
+
+pub type HandleRequestResult = std::result::Result<VmResponse, ()>;
+
+pub fn handle_request(request: &VmRequest, socket_path: &Path) -> HandleRequestResult {
+    match UnixSeqpacket::connect(&socket_path) {
+        Ok(s) => {
+            let socket = Tube::new(s);
+            if let Err(e) = socket.send(request) {
+                error!(
+                    "failed to send request to socket at '{:?}': {}",
+                    socket_path, e
+                );
+                return Err(());
+            }
+            match socket.recv() {
+                Ok(response) => Ok(response),
+                Err(e) => {
+                    error!(
+                        "failed to send request to socket at '{:?}': {}",
+                        socket_path, e
+                    );
+                    Err(())
+                }
+            }
+        }
+        Err(e) => {
+            error!("failed to connect to socket at '{:?}': {}", socket_path, e);
+            Err(())
+        }
+    }
+}
diff --git a/vm_control/src/lib.rs b/vm_control/src/lib.rs
index 7853b3d..88dabf7 100644
--- a/vm_control/src/lib.rs
+++ b/vm_control/src/lib.rs
@@ -13,6 +13,8 @@
 #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
 pub mod gdb;
 
+pub mod client;
+
 use std::fmt::{self, Display};
 use std::fs::File;
 use std::os::raw::c_int;
@@ -215,7 +217,7 @@
 }
 
 impl UsbControlAttachedDevice {
-    fn valid(self) -> bool {
+    pub fn valid(self) -> bool {
         self.port != 0
     }
 }