diff --git a/nvx-rs/examples/host-workflows.rs b/nvx-rs/examples/host-workflows.rs index 9a7fd64c4..8aedcf940 100644 --- a/nvx-rs/examples/host-workflows.rs +++ b/nvx-rs/examples/host-workflows.rs @@ -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; @@ -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, /// Why the capture or restore failed, and why its VM didn't stop. pub reason: String, } @@ -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( runtime: &Runtime, config: &VmConfig, directory: &Path, snapshot_dir: &Path, + request: R, deadline: Instant, -) -> Result> { +) -> Result> +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()); @@ -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) => { @@ -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 @@ -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)), @@ -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> { + 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, + _ => 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) -> Box { @@ -393,7 +424,7 @@ fn end(launched: Launched, error: Box) -> Box { Ok(_) => error, Err(failure) => Box::new(Unstopped { reason: format!("{error}, and its VM did not stop: {failure}"), - launched, + launched: Some(launched), }), } }