zlayer-hcs 0.12.0

Safe Rust wrapper for the Windows Host Compute Service (HCS)
Documentation
//! `ComputeSystem` lifecycle — create, open, start, shutdown, terminate,
//! pause/resume/save, query, modify.
//!
//! Every async method drives the HCS native async-operation model through
//! [`crate::operation::run_operation`], which handles the creation of the
//! backing `HCS_OPERATION`, registration of the completion trampoline, and
//! translation of the returned JSON / classified error back into an
//! ordinary `HcsResult`.
//!
//! # Handle ownership
//!
//! A `ComputeSystem` owns an [`OwnedSystem`] handle. Dropping the
//! `ComputeSystem` closes the caller's reference to the system via
//! `HcsCloseComputeSystem` — it does **not** terminate the system itself.
//! Use [`ComputeSystem::terminate`] to forcibly stop the system, or
//! [`ComputeSystem::shutdown`] for a graceful stop.

#![allow(clippy::missing_errors_doc)] // every method returns HcsResult; clippy pedantic complains otherwise.

use windows::core::{HRESULT, HSTRING};
use windows::Win32::System::HostComputeSystem::{
    HcsCreateComputeSystem, HcsGetComputeSystemProperties, HcsModifyComputeSystem,
    HcsOpenComputeSystem, HcsPauseComputeSystem, HcsResumeComputeSystem, HcsSaveComputeSystem,
    HcsShutDownComputeSystem, HcsStartComputeSystem, HcsTerminateComputeSystem, HCS_SYSTEM,
};

use crate::error::{HcsError, HcsResult};
use crate::handle::{OwnedSystem, SendHandle};
use crate::operation::run_operation;

/// A single HRESULT success value used to turn `windows_core::Result<()>`
/// into the `HRESULT` that `run_operation`'s closure must return.
const HR_OK: HRESULT = HRESULT(0);

/// Convert the `windows_core::Result<()>` returned by the windows-rs 0.62
/// HCS bindings into the `HRESULT` shape that [`run_operation`] expects in
/// its closure.
#[inline]
fn to_hresult(result: windows::core::Result<()>) -> HRESULT {
    match result {
        Ok(()) => HR_OK,
        Err(e) => e.code(),
    }
}

/// Owned compute system. All lifecycle calls take `&self`; the enclosed
/// [`OwnedSystem`] handle is closed on `Drop`.
#[derive(Debug)]
pub struct ComputeSystem {
    inner: OwnedSystem,
}

impl ComputeSystem {
    /// Create a new compute system.
    ///
    /// `id` is the caller-chosen container/VM identifier (often a GUID);
    /// `configuration_json` is the schema v2 compute-system document
    /// serialised with [`crate::schema::ComputeSystem`].
    ///
    /// The returned future resolves once HCS reports the system has
    /// transitioned to the `Created` state. The underlying handle is
    /// closed when the returned [`ComputeSystem`] is dropped, but the
    /// system itself is not terminated — call [`Self::shutdown`] or
    /// [`Self::terminate`] to stop it.
    pub async fn create(id: &str, configuration_json: &str) -> HcsResult<Self> {
        let id_w = HSTRING::from(id);
        let cfg_w = HSTRING::from(configuration_json);

        // HCS returns the compute-system handle synchronously on a
        // successful kickoff; the operation completion signals that the
        // system has finished transitioning to `Created`. We therefore
        // capture the handle in the closure via a mutable slot and then
        // await the operation for the state transition.
        //
        // The slot is wrapped in `SendHandle` so this local may cross the
        // `run_operation` `.await` below while keeping the enclosing
        // future `Send` — raw `HCS_SYSTEM` is `!Send + !Sync` because it
        // is transparent over `*mut c_void`.
        let mut handle_slot: Option<SendHandle<HCS_SYSTEM>> = None;

        let result = {
            let handle_slot = &mut handle_slot;
            run_operation(move |op| {
                // SAFETY: `id_w` and `cfg_w` live for the whole closure
                // invocation; `op` is a live HCS operation handle owned
                // by `run_operation`; security descriptor is None
                // (default — caller's token ACL).
                let res = unsafe { HcsCreateComputeSystem(&id_w, &cfg_w, op, None) };
                match res {
                    Ok(sys) => {
                        *handle_slot = Some(SendHandle(sys));
                        HR_OK
                    }
                    Err(e) => e.code(),
                }
            })
            .await
        };

        // Propagate a kickoff / completion failure before attempting to
        // wrap the handle (there is nothing to wrap in that case).
        let _json = result?;

        let Some(raw) = handle_slot else {
            return Err(HcsError::Other {
                hresult: 0,
                message: "HcsCreateComputeSystem returned success without a handle".to_string(),
            });
        };
        if raw.0.is_invalid() {
            return Err(HcsError::Other {
                hresult: 0,
                message: "HcsCreateComputeSystem returned an invalid handle".to_string(),
            });
        }

        // SAFETY: `raw.0` was just produced by a successful
        // `HcsCreateComputeSystem` call and has not been handed to any
        // other owner.
        let inner = unsafe { OwnedSystem::from_raw(raw.0) };
        Ok(Self { inner })
    }

    /// Open an existing compute system by id. This call is synchronous —
    /// there is no backing HCS operation.
    ///
    /// `requested_access` is a standard Windows access mask; `0` asks for
    /// the default access granted to the caller's token.
    pub fn open(id: &str, requested_access: u32) -> HcsResult<Self> {
        let id_w = HSTRING::from(id);
        // SAFETY: FFI call; `id_w` is a live HSTRING for the duration of
        // the call and `requested_access` is a plain integer.
        let raw = unsafe { HcsOpenComputeSystem(&id_w, requested_access) }
            .map_err(|e| HcsError::from_hresult(e.code(), format!("HcsOpenComputeSystem({id})")))?;

        if raw.is_invalid() {
            return Err(HcsError::Other {
                hresult: 0,
                message: "HcsOpenComputeSystem returned an invalid handle".to_string(),
            });
        }
        // SAFETY: `raw` came from a successful HCS open call.
        let inner = unsafe { OwnedSystem::from_raw(raw) };
        Ok(Self { inner })
    }

    /// Borrow the underlying raw HCS handle. Useful for low-level callers
    /// (e.g. event-callback registration) that need the bare pointer; the
    /// handle remains owned by this [`ComputeSystem`] and must not be
    /// closed by the caller.
    ///
    /// Returns a [`SendHandle<HCS_SYSTEM>`] rather than the bare raw
    /// handle so the wrapper's `Send + Sync` guarantees propagate to
    /// callers that hold the value across an `.await` — raw `HCS_SYSTEM`
    /// is transparent over `*mut c_void` and is therefore `!Send + !Sync`.
    /// FFI call sites dereference with `*handle` (via [`std::ops::Deref`])
    /// when passing the value to a C entry point.
    #[must_use]
    pub fn raw(&self) -> SendHandle<HCS_SYSTEM> {
        self.inner.as_raw()
    }

    /// Start the compute system. `options_json` is an optional JSON
    /// options document; pass `""` to use defaults.
    pub async fn start(&self, options_json: &str) -> HcsResult<()> {
        let opts_w = HSTRING::from(options_json);
        // Wrap the raw handle so the enclosing future stays `Send` across
        // the `.await` below. See `handle::SendHandle` for the rationale.
        //
        // NOTE: Inside the closure we MUST dereference through `*handle`
        // (not `handle.0`). Rust 2021's disjoint-field capture rules cause
        // `handle.0` in a `move` closure to capture ONLY the inner
        // `HCS_SYSTEM` field (which is `!Send + !Sync`), defeating the
        // `SendHandle` wrapper. `*handle` goes through `Deref`, which
        // requires `&handle` and therefore captures the whole wrapper.
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: `*handle` is live for the duration of `self`, which
            // outlives this await (borrow check); `opts_w` lives for the
            // closure invocation; `op` is owned by `run_operation`.
            to_hresult(unsafe { HcsStartComputeSystem(*handle, op, &opts_w) })
        })
        .await?;
        Ok(())
    }

    /// Shut down the compute system gracefully. `options_json` may carry
    /// a shutdown timeout per the HCS schema.
    pub async fn shutdown(&self, options_json: &str) -> HcsResult<()> {
        let opts_w = HSTRING::from(options_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start` — identical lifetime argument.
            to_hresult(unsafe { HcsShutDownComputeSystem(*handle, op, &opts_w) })
        })
        .await?;
        Ok(())
    }

    /// Forcefully terminate the compute system without a grace period.
    pub async fn terminate(&self, options_json: &str) -> HcsResult<()> {
        let opts_w = HSTRING::from(options_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start`.
            to_hresult(unsafe { HcsTerminateComputeSystem(*handle, op, &opts_w) })
        })
        .await?;
        Ok(())
    }

    /// Pause the compute system. Supported on Hyper-V-isolated systems;
    /// process-isolated containers will typically return a failure
    /// HRESULT here.
    pub async fn pause(&self, options_json: &str) -> HcsResult<()> {
        let opts_w = HSTRING::from(options_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start`.
            to_hresult(unsafe { HcsPauseComputeSystem(*handle, op, &opts_w) })
        })
        .await?;
        Ok(())
    }

    /// Resume a previously paused compute system.
    pub async fn resume(&self, options_json: &str) -> HcsResult<()> {
        let opts_w = HSTRING::from(options_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start`.
            to_hresult(unsafe { HcsResumeComputeSystem(*handle, op, &opts_w) })
        })
        .await?;
        Ok(())
    }

    /// Save the compute system's runtime state to disk.
    pub async fn save(&self, options_json: &str) -> HcsResult<()> {
        let opts_w = HSTRING::from(options_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start`.
            to_hresult(unsafe { HcsSaveComputeSystem(*handle, op, &opts_w) })
        })
        .await?;
        Ok(())
    }

    /// Read one or more properties. `property_query_json` is a JSON
    /// `PropertyQuery` document such as
    /// `{"PropertyTypes":["Statistics"]}`. Returns the raw JSON response
    /// from HCS; callers deserialize via [`crate::schema::Statistics`] (or
    /// a sibling property type) as appropriate.
    pub async fn properties(&self, property_query_json: &str) -> HcsResult<String> {
        let query_w = HSTRING::from(property_query_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start`.
            to_hresult(unsafe { HcsGetComputeSystemProperties(*handle, op, &query_w) })
        })
        .await
    }

    /// Apply a modification to a running compute system. Submits
    /// `HcsModifyComputeSystem` with the JSON serialisation of `request`.
    /// Matches hcsshim's `internal/hcs/system.go::HcsSystem::Modify`.
    ///
    /// Most callers should prefer this typed entry point over
    /// [`Self::modify_raw_json`]; the raw variant is reserved for
    /// modification shapes we have not yet typed in
    /// [`crate::schema::ModifySettingRequest`].
    pub async fn modify(&self, request: &crate::schema::ModifySettingRequest) -> HcsResult<()> {
        let json = serde_json::to_string(request)?;
        self.modify_raw_json(&json).await
    }

    /// Raw-JSON variant of [`Self::modify`] for callers that need a
    /// `ModifySettingRequest` shape this crate does not yet expose typed.
    /// `modification_json` is a JSON `ModifySettingRequest` document per
    /// the hcsshim schema.
    pub async fn modify_raw_json(&self, modification_json: &str) -> HcsResult<()> {
        let mod_w = HSTRING::from(modification_json);
        let handle = self.inner.as_raw();
        run_operation(move |op| {
            // SAFETY: see `start`; the fourth argument (optional identity
            // handle) is `None` — we do not attach a token to the request.
            to_hresult(unsafe { HcsModifyComputeSystem(*handle, op, &mod_w, None) })
        })
        .await?;
        Ok(())
    }

    /// Convenience: hot-add a `VirtualSMB` share to a running VM.
    ///
    /// The resource path is the share COLLECTION
    /// (`"VirtualMachine/Devices/VirtualSmb/Shares"`), NOT an indexed element:
    /// HCS keys VSMB shares by their `Name`, so an `Add` targets the collection
    /// and HCS appends. Including a trailing `/<index>` makes the modify fail
    /// with `0x80070057` (`E_INVALIDARG`). Mirrors hcsshim's
    /// `resourcepaths.VSMBShareResourcePath`.
    pub async fn add_vsmb(&self, share: &crate::schema::VirtualSmbShare) -> HcsResult<()> {
        let req = crate::schema::ModifySettingRequest {
            resource_path: "VirtualMachine/Devices/VirtualSmb/Shares".to_string(),
            request_type: crate::schema::ModifyRequestType::Add,
            settings: Some(serde_json::to_value(share)?),
            guest_request: None,
        };
        self.modify(&req).await
    }

    /// Convenience: hot-add a SCSI attachment to a running VM.
    /// `controller_guid` is the SCSI controller's GUID key — the SAME string the
    /// create-time doc keys `Devices.Scsi` by. HCS resolves the controller by
    /// that key, NOT a bare ordinal, so a numeric index yields
    /// `ERROR_NOT_FOUND` (`0x80070490`). `lun` is the LUN under that controller.
    /// Matches hcsshim's `resourcepaths.SCSIResourceFormat` (controller GUID +
    /// LUN).
    pub async fn add_scsi(
        &self,
        controller_guid: &str,
        lun: usize,
        attachment: &crate::schema::ScsiAttachment,
    ) -> HcsResult<()> {
        let req = crate::schema::ModifySettingRequest {
            resource_path: format!(
                "VirtualMachine/Devices/Scsi/{controller_guid}/Attachments/{lun}"
            ),
            request_type: crate::schema::ModifyRequestType::Add,
            settings: Some(serde_json::to_value(attachment)?),
            guest_request: None,
        };
        self.modify(&req).await
    }
}