#![allow(clippy::missing_errors_doc)]
use core::ffi::c_void;
use windows::core::{GUID, HSTRING, PWSTR};
use windows::Win32::System::HostComputeNetwork::{
HcnCreateEndpoint, HcnDeleteEndpoint, HcnEnumerateEndpoints, HcnModifyEndpoint,
HcnOpenEndpoint, HcnQueryEndpointProperties, HcnQueryEndpointStats,
};
use crate::error::{HnsError, HnsResult};
use crate::handle::{HcnEndpointHandle, OwnedEndpoint};
use crate::schema::{EndpointStats, HostComputeEndpoint};
#[derive(Debug)]
pub struct Endpoint {
id: GUID,
handle: OwnedEndpoint,
}
impl Endpoint {
pub fn create(
network_id: GUID,
endpoint_id: GUID,
settings: &HostComputeEndpoint,
) -> HnsResult<Self> {
let mut ep_settings = settings.clone();
ep_settings.host_compute_network = format!("{network_id:?}");
let settings_json = serde_json::to_string(&ep_settings)?;
let settings_hstring = HSTRING::from(settings_json);
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnCreateEndpoint(
core::ptr::null(),
&endpoint_id,
&settings_hstring,
&mut raw,
Some(&mut err_record),
)
.map_err(|e| classify_error(e.code(), err_record, "HcnCreateEndpoint"))?;
}
if raw.is_null() {
return Err(HnsError::Other {
hresult: 0,
message: "HcnCreateEndpoint returned null handle".to_string(),
});
}
let handle = unsafe { OwnedEndpoint::from_raw(raw as HcnEndpointHandle) };
Ok(Self {
id: endpoint_id,
handle,
})
}
pub fn open(id: GUID) -> HnsResult<Self> {
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnOpenEndpoint(&id, &mut raw, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnOpenEndpoint({id:?})"))
})?;
}
if raw.is_null() {
return Err(HnsError::NotFound {
id: format!("{id:?}"),
});
}
Ok(Self {
id,
handle: unsafe { OwnedEndpoint::from_raw(raw as HcnEndpointHandle) },
})
}
pub fn delete(id: GUID) -> HnsResult<()> {
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnDeleteEndpoint(&id, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnDeleteEndpoint({id:?})"))
})?;
}
Ok(())
}
pub fn modify(&self, modification_json: &str) -> HnsResult<()> {
let mod_hstring = HSTRING::from(modification_json);
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnModifyEndpoint(self.handle.as_raw(), &mod_hstring, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnModifyEndpoint"))?;
}
Ok(())
}
pub fn query_properties(&self, query_json: &str) -> HnsResult<HostComputeEndpoint> {
let query_hstring = HSTRING::from(query_json);
let mut out_properties: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnQueryEndpointProperties(
self.handle.as_raw(),
&query_hstring,
&mut out_properties,
Some(&mut err_record),
)
.map_err(|e| classify_error(e.code(), err_record, "HcnQueryEndpointProperties"))?;
}
let json = decode_pwstr(out_properties);
let parsed: HostComputeEndpoint = serde_json::from_str(&json)?;
Ok(parsed)
}
pub fn query_stats(&self, query_json: &str) -> HnsResult<EndpointStats> {
let query_hstring = HSTRING::from(query_json);
let mut out_stats: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnQueryEndpointStats(
self.handle.as_raw(),
&query_hstring,
&mut out_stats,
Some(&mut err_record),
)
.map_err(|e| classify_error(e.code(), err_record, "HcnQueryEndpointStats"))?;
}
let json = decode_pwstr(out_stats);
let parsed: EndpointStats = serde_json::from_str(&json)?;
Ok(parsed)
}
pub fn primary_ip(&self) -> HnsResult<Option<String>> {
let props = self.query_properties("{}")?;
Ok(props
.ip_configurations
.first()
.map(|ic| ic.ip_address.clone()))
}
#[must_use]
pub fn id(&self) -> GUID {
self.id
}
#[must_use]
pub fn handle(&self) -> &OwnedEndpoint {
&self.handle
}
}
pub fn list(query_json: &str) -> HnsResult<Vec<GUID>> {
let query_hstring = HSTRING::from(query_json);
let mut out_endpoints: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnEnumerateEndpoints(&query_hstring, &mut out_endpoints, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnEnumerateEndpoints"))?;
}
let json = decode_pwstr(out_endpoints);
let arr: Vec<String> = if json.is_empty() {
Vec::new()
} else {
serde_json::from_str(&json).unwrap_or_default()
};
let mut guids = Vec::with_capacity(arr.len());
for s in arr {
let bare = s.trim_matches(|c: char| c == '{' || c == '}');
let guid = GUID::try_from(bare).map_err(|e| HnsError::Other {
hresult: 0,
message: format!("bad GUID from HcnEnumerateEndpoints: {s}: {e}"),
})?;
guids.push(guid);
}
Ok(guids)
}
fn decode_pwstr(p: PWSTR) -> String {
use windows::Win32::Foundation::{LocalFree, HLOCAL};
if p.is_null() {
return String::new();
}
let s = unsafe { p.to_string().unwrap_or_default() };
unsafe {
let _ = LocalFree(Some(HLOCAL(p.0.cast())));
}
s
}
fn classify_error<S: Into<String>>(
hr: windows::core::HRESULT,
err_record: PWSTR,
context: S,
) -> HnsError {
let ctx: String = context.into();
let decoded = decode_pwstr(err_record);
let msg = if decoded.is_empty() {
ctx
} else {
format!("{ctx}: {decoded}")
};
HnsError::from_hresult(hr, msg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_pwstr_null_returns_empty() {
let s = decode_pwstr(PWSTR::null());
assert!(s.is_empty());
}
#[test]
fn classify_error_access_denied_hresult() {
let err = classify_error(windows::core::HRESULT(-0x7FFF_FFFB), PWSTR::null(), "ctx");
assert!(matches!(err, HnsError::AccessDenied { .. }));
}
#[test]
fn classify_error_preserves_context_when_errrecord_empty() {
let err = classify_error(
windows::core::HRESULT(-0x1234_5678),
PWSTR::null(),
"HcnCreateEndpoint",
);
if let HnsError::Other { message, .. } = err {
assert_eq!(message, "HcnCreateEndpoint");
} else {
panic!("expected Other, got {err:?}");
}
}
}