Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions nvx-rs/examples/host-workflows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use nvx::host::restore::{
use nvx::host::runtime::Runtime;
use nvx::host::session::{self, ControlSession, SessionError};
use nvx::host::snapshot::{self, Snapshot};
use nvx::host::vm::{self, Launched, Reattached, SavedVm, Stopped, VmDir, VmOutputs};
use nvx::host::vm::{self, Launched, Prepared, Reattached, SavedVm, Stopped, VmDir, VmOutputs};

/// One byte past OpenVMM's 25-byte restore-ready event, so that a longer event fails to parse.
const READY_LIMIT: usize = 26;
Expand All @@ -51,13 +51,14 @@ pub struct Saved {
}

/// A capture or restore that failed once its VM started, and whose VM didn't show its exit when
/// the caller ended it. The caller takes it from the error with `downcast`, and keeps the VM, to
/// stop it again, with its directory, and a restore's view and lock of the snapshot, until the
/// VM exits.
/// the spawn or the caller ended it. The caller takes it from the error with `downcast`, and
/// keeps the VM, to stop it again, with its directory, and a restore's view and lock of the
/// snapshot, until the VM exits, or, without the VM, until the host restarts.
#[derive(Debug)]
pub struct Unstopped {
/// The VM, whose process may still run.
pub launched: Launched,
/// The VM, whose process may still run, or `None` after a spawn that could not confirm the
/// exit of the process that it started, which the caller can then neither stop nor wait for.
pub launched: Option<Launched>,
/// Why the capture or restore failed, and why its VM didn't stop.
pub reason: String,
}
Expand Down Expand Up @@ -100,21 +101,28 @@ pub fn cold_boot(
Ok((launched, saved))
}

/// Captures a snapshot of `config`, whose boot asks for a capture, in `directory`, a new private
/// Captures a snapshot of `config`, whose boot has a `capture`, in `directory`, a new private
/// directory, moves it to `snapshot_dir`, another, which must be empty, and opens it there.
///
/// The guest captures itself, as the guest example shows, and its VM exits once the capture
/// `request` asks the guest for a snapshot, in the caller's own protocol with its guest, on a
/// control session that only the VM's own process may serve, and returns once the guest answered.
/// The guest then captures the VM, as the guest example shows, and the VM exits once the capture
/// commits. The caller finishes the capture with the proof of that exit, then moves the
/// snapshot's three entries, and nothing else, out of the VM directory. It keeps them private
/// and immutable while it restores from them. A VM that doesn't exit by `deadline` captures
/// nothing, and the caller ends it, or keeps it in an [`Unstopped`] error.
pub fn capture(
/// and immutable while it restores from them. When the request fails, or the VM doesn't exit by
/// `deadline`, the caller finishes no snapshot: it ends the VM, or keeps it in an [`Unstopped`]
/// error.
pub fn capture<R>(
runtime: &Runtime,
config: &VmConfig,
directory: &Path,
snapshot_dir: &Path,
request: R,
deadline: Instant,
) -> Result<Snapshot, Box<dyn Error>> {
) -> Result<Snapshot, Box<dyn Error>>
where
R: FnOnce(&mut ControlSession, Instant) -> Result<(), SessionError>,
{
vm::create_private_dir(snapshot_dir)?;
if fs::read_dir(snapshot_dir)?.next().is_some() {
return Err("the snapshot's directory is not empty".into());
Expand All @@ -126,8 +134,20 @@ pub fn capture(
let context = prepared
.capture_context()
.cloned()
.ok_or("the configuration's boot asks for no capture")?;
let launched = vm::spawn(prepared)?;
.ok_or("the configuration's boot has no capture")?;
let launched = start(prepared)?;
let process = launched.process.reference();
let requested = session::connect(
&launched.outputs.endpoint,
process.pid,
process.start_time,
deadline,
)
.and_then(|stream| ControlSession::attach(stream, launched.capability.as_bytes(), deadline))
.and_then(|mut session| request(&mut session, deadline));
if let Err(error) = requested {
return Err(end(launched, error.into()));
}
let exited = match launched.process.wait(deadline) {
Ok(Some(exited)) => exited,
Ok(None) => {
Expand Down Expand Up @@ -155,7 +175,7 @@ pub fn capture(
/// which every caller of the snapshot takes before it opens it, through the restore-ready event
/// or the VM's verified exit, and around the view's removal. NVX takes no lock, and Windows
/// needs none. The caller holds it through this call, which ends the VM when the restore fails,
/// or returns the VM in an [`Unstopped`] error, which the caller keeps.
/// or fails with an [`Unstopped`] error, after which the caller keeps holding it.
///
/// `activate` sends the activation record's bytes as the first request of the caller's own
/// protocol with its guest, and returns the activation that the guest's Ready reports. The
Expand All @@ -176,8 +196,7 @@ where
let dir = VmDir::open(directory)?;
let prepared = vm::create(runtime, config, Some(snapshot), &dir)?;
let record = prepared.activation_record(transaction)?;
// A spawn that fails stops the child that it started.
let mut launched = vm::spawn(prepared)?;
let mut launched = start(prepared)?;
match drive(&mut launched, &record, activate, deadline) {
Ok(session) => Ok((launched, session)),
Err(error) => Err(end(launched, error)),
Expand Down Expand Up @@ -379,6 +398,18 @@ pub fn stop(
vm::stop(process, outputs, request, Instant::now() + grace, deadline)
}

/// Starts a capture's or restore's VM. A spawn that fails ends the process that it started; one
/// that cannot confirm that exit fails here with an [`Unstopped`] error without the VM.
fn start(prepared: Prepared) -> Result<Launched, Box<dyn Error>> {
vm::spawn(prepared).map_err(|error| match error.kind() {
ErrorKind::StillRunning => Box::new(Unstopped {
reason: format!("{error}, and the process that it started may still run"),
launched: None,
}) as Box<dyn Error>,
_ => error.into(),
})
}

/// Ends a VM that its caller gives up on after `error`, at once, and returns `error` once the
/// VM's exit is verified, or an [`Unstopped`] error that returns the VM to the caller.
fn end(launched: Launched, error: Box<dyn Error>) -> Box<dyn Error> {
Expand All @@ -393,7 +424,7 @@ fn end(launched: Launched, error: Box<dyn Error>) -> Box<dyn Error> {
Ok(_) => error,
Err(failure) => Box::new(Unstopped {
reason: format!("{error}, and its VM did not stop: {failure}"),
launched,
launched: Some(launched),
}),
}
}
Loading