use super::handle::WslcContainerHandle;
use super::settings;
use super::types::{ContainerPortMappingData, RetainedContainerMetadata};
use crate::error::WslcError;
use crate::process::ProcessBuilder;
use crate::session::WslcSessionHandle;
use std::ffi::CString;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use wslcsdk_sys::types::{
WslcContainer, WslcContainerFlags, WslcContainerNetworkingMode, WslcContainerSettings,
WslcPortProtocol,
};
use wslcsdk_sys::*;
#[derive(Debug)]
pub struct ContainerBuilder {
pub(super) image_name: String,
pub(super) name: Option<String>,
pub(super) init_process: Option<ProcessBuilder>,
pub(super) networking_mode: Option<WslcContainerNetworkingMode>,
pub(super) host_name: Option<String>,
pub(super) domain_name: Option<String>,
pub(super) flags: WslcContainerFlags,
pub(super) port_mappings: Vec<ContainerPortMappingData>,
pub(super) volumes: Vec<(PathBuf, String, bool)>,
pub(super) named_volumes: Vec<(String, String, bool)>,
}
impl ContainerBuilder {
pub fn new(image_name: impl Into<String>) -> Self {
Self {
image_name: image_name.into(),
name: None,
init_process: None,
networking_mode: None,
host_name: None,
domain_name: None,
flags: 0,
port_mappings: Vec::new(),
volumes: Vec::new(),
named_volumes: Vec::new(),
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn init_process(mut self, process: ProcessBuilder) -> Self {
self.init_process = Some(process);
self
}
pub fn networking_mode(mut self, mode: WslcContainerNetworkingMode) -> Self {
self.networking_mode = Some(mode);
self
}
pub fn host_name(mut self, host_name: impl Into<String>) -> Self {
self.host_name = Some(host_name.into());
self
}
pub fn domain_name(mut self, domain_name: impl Into<String>) -> Self {
self.domain_name = Some(domain_name.into());
self
}
pub fn flags(mut self, flags: WslcContainerFlags) -> Self {
self.flags = flags;
self
}
pub fn get_flags(&self) -> WslcContainerFlags {
self.flags
}
pub fn auto_remove(mut self, enable: bool) -> Self {
if enable {
self.flags |= WSLC_CONTAINER_FLAG_AUTO_REMOVE;
} else {
self.flags &= !WSLC_CONTAINER_FLAG_AUTO_REMOVE;
}
self
}
pub fn privileged(mut self, enable: bool) -> Self {
if enable {
self.flags |= WSLC_CONTAINER_FLAG_PRIVILEGED;
} else {
self.flags &= !WSLC_CONTAINER_FLAG_PRIVILEGED;
}
self
}
pub fn enable_gpu(mut self, enable: bool) -> Self {
if enable {
self.flags |= WSLC_CONTAINER_FLAG_ENABLE_GPU;
} else {
self.flags &= !WSLC_CONTAINER_FLAG_ENABLE_GPU;
}
self
}
pub fn add_port_mapping(
mut self,
windows_port: u16,
container_port: u16,
protocol: WslcPortProtocol,
) -> Self {
self.port_mappings.push(ContainerPortMappingData {
windows_port,
container_port,
protocol,
bind_ip: None,
});
self
}
pub fn add_port_mapping_with_ip(
mut self,
windows_port: u16,
container_port: u16,
protocol: WslcPortProtocol,
bind_ip: IpAddr,
) -> Self {
self.port_mappings.push(ContainerPortMappingData {
windows_port,
container_port,
protocol,
bind_ip: Some(bind_ip),
});
self
}
pub fn add_port_mapping_str(mut self, mapping_str: &str) -> Result<Self, WslcError> {
let mapping: ContainerPortMappingData = mapping_str.parse()?;
self.port_mappings.push(mapping);
Ok(self)
}
pub fn add_volume(
mut self,
windows_path: impl AsRef<Path>,
container_path: &str,
read_only: bool,
) -> Self {
self.volumes.push((
windows_path.as_ref().to_path_buf(),
container_path.to_string(),
read_only,
));
self
}
pub fn add_named_volume(mut self, name: &str, container_path: &str, read_only: bool) -> Self {
self.named_volumes
.push((name.to_string(), container_path.to_string(), read_only));
self
}
pub fn build(self, session: &WslcSessionHandle) -> Result<WslcContainerHandle, WslcError> {
let _com_guard = crate::com::try_initialize_mta()?;
let c_image = CString::new(self.image_name.as_str())
.map_err(|e| WslcError::NulError(format!("镜像名称含非法空字节: {e}")))?;
let mut settings = WslcContainerSettings::default();
let hr = unsafe { WslcInitContainerSettings(c_image.as_ptr(), &mut settings) };
WslcError::check_hr(hr, "初始化容器配置失败")?;
let basic = settings::apply_metadata(&self, &mut settings)?;
let (ports, sockaddrs) = settings::apply_port_mappings(&self, &mut settings)?;
let volumes = settings::apply_volumes(&self, &mut settings)?;
let named_volumes = settings::apply_named_volumes(&self, &mut settings)?;
let (init_retained, init_stream_state) = if let Some(init_builder) = self.init_process {
let stream_state = init_builder.stream_state();
let mut retained = init_builder.build_raw_settings()?;
let hr = unsafe {
WslcSetContainerSettingsInitProcess(&mut settings, &mut retained.settings)
};
WslcError::check_hr(hr, "设置容器主进程配置失败")?;
(Some(retained), stream_state)
} else {
(None, None)
};
let _retained_data = RetainedContainerMetadata {
_image: c_image,
_basic: basic,
_ports: ports,
_sockaddrs: sockaddrs,
_volumes: volumes,
_named_volumes: named_volumes,
_init_process: init_retained,
};
let mut raw_container = WslcContainer::NULL;
let mut err_msg: *mut u16 = std::ptr::null_mut();
let hr = unsafe {
WslcCreateContainer(
session.as_raw(),
&settings,
&mut raw_container,
&mut err_msg,
)
};
unsafe {
WslcError::check(hr, err_msg)?;
}
if raw_container.is_null() {
return Err(WslcError::InvalidHandle);
}
log::info!(
"WSLC 容器创建成功,基础镜像: '{}',所属会话: '{}'",
self.image_name,
session.name()
);
WslcContainerHandle::from_raw_inner(raw_container, session.clone(), init_stream_state)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_session() -> WslcSessionHandle {
unsafe { WslcSessionHandle::from_raw(WslcSession::NULL, "fake-session") }
}
#[test]
fn test_flag_setters_toggle_independent_bits() {
let builder = ContainerBuilder::new("ubuntu:latest")
.name("test-container")
.auto_remove(true);
assert_eq!(builder.get_flags() & WSLC_CONTAINER_FLAG_AUTO_REMOVE, 1);
let privileged = ContainerBuilder::new("ubuntu:latest").privileged(true);
assert_eq!(
privileged.get_flags() & WSLC_CONTAINER_FLAG_PRIVILEGED,
WSLC_CONTAINER_FLAG_PRIVILEGED
);
assert_eq!(privileged.get_flags() & WSLC_CONTAINER_FLAG_AUTO_REMOVE, 0);
let cleared = ContainerBuilder::new("ubuntu:latest")
.auto_remove(true)
.enable_gpu(true)
.auto_remove(false);
assert_eq!(cleared.get_flags() & WSLC_CONTAINER_FLAG_AUTO_REMOVE, 0);
assert_eq!(
cleared.get_flags() & WSLC_CONTAINER_FLAG_ENABLE_GPU,
WSLC_CONTAINER_FLAG_ENABLE_GPU
);
}
#[test]
fn test_port_mapping_with_explicit_bind_ip() {
let builder = ContainerBuilder::new("ubuntu:latest")
.add_port_mapping_with_ip(
8080,
80,
WslcPortProtocol::Tcp,
"127.0.0.1".parse().unwrap(),
)
.add_port_mapping_with_ip(9090, 90, WslcPortProtocol::Udp, "::1".parse().unwrap());
assert_eq!(builder.get_flags(), 0);
}
#[test]
fn test_port_mapping_str_chaining() {
let builder = ContainerBuilder::new("ubuntu:latest")
.add_port_mapping_str("8080:80")
.expect("IPv4 端口映射解析失败")
.add_port_mapping_str("127.0.0.1:9000:9000/udp")
.expect("带绑定地址的端口映射解析失败");
assert_eq!(builder.get_flags(), 0);
assert!(
ContainerBuilder::new("ubuntu:latest")
.add_port_mapping_str("not-a-mapping")
.is_err()
);
}
#[test]
fn test_volume_validation_rejects_invalid_mounts() {
let session = fake_session();
let empty_host = ContainerBuilder::new("ubuntu:latest").add_volume("", "/data", false);
assert!(empty_host.build(&session).is_err());
let relative_container =
ContainerBuilder::new("ubuntu:latest").add_volume("C:\\data", "relative/path", false);
assert!(relative_container.build(&session).is_err());
let empty_name =
ContainerBuilder::new("ubuntu:latest").add_named_volume(" ", "/data", false);
assert!(empty_name.build(&session).is_err());
let relative_named =
ContainerBuilder::new("ubuntu:latest").add_named_volume("myvol", "data", false);
assert!(relative_named.build(&session).is_err());
}
#[test]
fn test_container_with_plain_init_process() {
let process_builder = ProcessBuilder::new()
.command(&["/bin/bash", "-c", "echo hello"])
.working_directory("/workspace")
.env("ENV_KEY", "ENV_VAL");
let builder = ContainerBuilder::new("ubuntu:latest")
.name("test-container")
.init_process(process_builder);
assert_eq!(builder.get_flags(), 0);
}
#[test]
fn test_container_with_streaming_init_process_survives_early_stream_drop() {
let (proc_builder, streams) = ProcessBuilder::new()
.command(&["/bin/sh"])
.with_streaming_io();
drop(streams);
let builder = ContainerBuilder::new("alpine:latest").init_process(proc_builder);
assert_eq!(builder.get_flags(), 0);
}
}