use std::sync::Arc;
use std::time::Duration;
use arcbox_vm_driver::net::NetworkIdentity;
use arcbox_vm_driver::{PreparedVm, VmHandle, Vsock, VsockListener};
use async_trait::async_trait;
use tokio::sync::mpsc;
use super::files;
use super::{HandleVsock, READY_PORT, clock, connect_to_port, exec, net, wait_port, wait_ready};
use crate::agent::{
ClockSync, DirWatch, ExecInput, GuestAgent, GuestAgentFactory, GuestFiles, OutputChunk,
PortWait, ProbeGate, Readiness, ReadyGate, StartCommand,
};
use crate::boot_proto::NetReconfigCommand;
use crate::error::Result;
use crate::file_proto::FileStatDto;
pub struct VmProtoAgent {
vsock: Arc<dyn Vsock>,
}
impl VmProtoAgent {
#[must_use]
pub fn over(handle: Arc<dyn VmHandle>) -> Self {
Self {
vsock: Arc::new(HandleVsock(handle)),
}
}
}
#[async_trait]
impl GuestAgent for VmProtoAgent {
async fn run(&self, start: StartCommand) -> Result<mpsc::Receiver<Result<OutputChunk>>> {
exec::run(self.vsock.as_ref(), start).await
}
async fn exec(
&self,
start: StartCommand,
) -> Result<(ExecInput, mpsc::Receiver<Result<OutputChunk>>)> {
exec::exec(self.vsock.as_ref(), start).await
}
async fn sync_clock(&self) -> Result<ClockSync> {
clock::sync_clock(self.vsock.as_ref()).await
}
async fn reconfigure_network(&self, cmd: &NetReconfigCommand) -> Result<()> {
net::reconfigure_network(self.vsock.as_ref(), cmd).await
}
async fn wait_for_port(&self, port: u16, timeout: Duration) -> Result<PortWait> {
wait_port::wait_for_port(self.vsock.as_ref(), port, timeout).await
}
fn files(&self) -> &dyn GuestFiles {
self
}
}
#[async_trait]
impl GuestFiles for VmProtoAgent {
async fn read(&self, path: &str) -> Result<Vec<u8>> {
files::read_file(self.vsock.as_ref(), path).await
}
async fn write(&self, path: &str, mode: u32, data: &[u8]) -> Result<()> {
files::write_file(self.vsock.as_ref(), path, mode, data).await
}
async fn stat(&self, path: &str) -> Result<FileStatDto> {
files::stat_file(self.vsock.as_ref(), path).await
}
async fn list(&self, path: &str) -> Result<Vec<FileStatDto>> {
files::list_dir(self.vsock.as_ref(), path).await
}
async fn make_dir(&self, path: &str, mode: u32) -> Result<()> {
files::make_dir(self.vsock.as_ref(), path, mode).await
}
async fn remove(&self, path: &str, recursive: bool) -> Result<()> {
files::remove_entry(self.vsock.as_ref(), path, recursive).await
}
async fn rename(&self, from: &str, to: &str) -> Result<()> {
files::move_entry(self.vsock.as_ref(), from, to).await
}
async fn watch(&self, path: &str, recursive: bool) -> Result<DirWatch> {
files::watch_dir(self.vsock.as_ref(), path, recursive).await
}
}
pub struct VmProtoAgentFactory {
readiness: Readiness,
}
impl Default for VmProtoAgentFactory {
fn default() -> Self {
Self {
readiness: Readiness::DialOut { port: READY_PORT },
}
}
}
impl VmProtoAgentFactory {
#[must_use]
pub const fn with_readiness(readiness: Readiness) -> Self {
Self { readiness }
}
}
#[async_trait]
impl GuestAgentFactory for VmProtoAgentFactory {
fn readiness(&self) -> Readiness {
self.readiness
}
async fn arm_readiness(&self, prepared: &dyn PreparedVm) -> Result<Box<dyn ReadyGate>> {
match self.readiness {
Readiness::DialOut { port } => {
let listener = match prepared.vsock_listener() {
Some(listen) => listen.listen(port).await,
None => Err(arcbox_vm_driver::Error::InvalidSpec(
"the vm driver cannot listen for the guest's readiness dial-out".into(),
)),
}?;
Ok(Box::new(DialOutGate { listener }))
}
Readiness::Poll { port } => Ok(Box::new(PollGate { port })),
Readiness::Probe => Ok(Box::new(ProbeGate)),
}
}
fn connect(
&self,
handle: Arc<dyn VmHandle>,
_net: Option<&NetworkIdentity>,
) -> Result<Arc<dyn GuestAgent>> {
Ok(Arc::new(VmProtoAgent::over(handle)))
}
}
struct DialOutGate {
listener: Box<dyn VsockListener>,
}
#[async_trait]
impl ReadyGate for DialOutGate {
async fn wait(
&mut self,
_handle: &Arc<dyn VmHandle>,
_agent: &Arc<dyn GuestAgent>,
) -> Result<()> {
wait_ready(&mut *self.listener).await
}
}
struct PollGate {
port: u32,
}
#[async_trait]
impl ReadyGate for PollGate {
async fn wait(
&mut self,
handle: &Arc<dyn VmHandle>,
_agent: &Arc<dyn GuestAgent>,
) -> Result<()> {
let vsock = HandleVsock(Arc::clone(handle));
connect_to_port(&vsock, self.port).await.map(drop)
}
}
#[cfg(test)]
mod tests {
use std::io::Write as _;
use arcbox_vm_driver::testkit::FakeDriver;
use arcbox_vm_driver::{
BootSpec, ConsoleSpec, IsolationSpec, VmDriver as _, VmId, VmSpec, VsockSpec,
};
use super::*;
fn vm_spec(id: &VmId) -> VmSpec {
VmSpec {
id: id.clone(),
cpus: 1,
memory_mib: 128,
boot: BootSpec::Kernel {
image: "/vmlinux".into(),
cmdline: String::new(),
initrd: None,
},
disks: vec![],
nics: vec![],
vsock: Some(VsockSpec { guest_cid: 3 }),
shares: vec![],
console: ConsoleSpec::Off,
balloon: false,
entropy: false,
dirty_tracking: false,
isolation: IsolationSpec::None,
}
}
async fn armed(
readiness: Readiness,
dir: &std::path::Path,
) -> (
FakeDriver,
VmId,
Arc<dyn VmHandle>,
Arc<dyn GuestAgent>,
Box<dyn ReadyGate>,
) {
let driver = FakeDriver::new();
let id = VmId::new("gate").unwrap();
let prepared = driver
.prepare()
.unwrap()
.prepare(&id, &IsolationSpec::None, dir)
.await
.unwrap();
let factory = VmProtoAgentFactory::with_readiness(readiness);
let gate = factory.arm_readiness(prepared.as_ref()).await.unwrap();
let handle: Arc<dyn VmHandle> = Arc::from(prepared.boot(vm_spec(&id)).await.unwrap());
let agent = factory.connect(Arc::clone(&handle), None).unwrap();
(driver, id, handle, agent, gate)
}
#[tokio::test]
async fn dial_out_gate_takes_the_guests_one_byte() {
let dir = tempfile::tempdir().unwrap();
let (driver, id, handle, agent, mut gate) =
armed(Readiness::DialOut { port: READY_PORT }, dir.path()).await;
let mut guest = driver.guest_dial(&id, READY_PORT).unwrap();
guest.write_all(&[1]).unwrap();
gate.wait(&handle, &agent).await.unwrap();
}
#[tokio::test]
async fn poll_gate_dials_the_booted_guest() {
let dir = tempfile::tempdir().unwrap();
let (_driver, _id, handle, agent, mut gate) =
armed(Readiness::Poll { port: 52 }, dir.path()).await;
gate.wait(&handle, &agent).await.unwrap();
}
}