use super::handle::WslcProcessHandle;
use super::stream::{ProcessStreams, StreamState, setup_streaming_channels};
use crate::container::WslcContainerHandle;
use crate::error::WslcError;
use core::ffi::c_void;
use std::ffi::CString;
use std::sync::Arc;
use wslcsdk_sys::types::{
WslcProcess, WslcProcessCallbacks, WslcProcessFlags, WslcProcessSettings,
};
use wslcsdk_sys::*;
pub(crate) struct RetainedProcessSettings {
pub(crate) settings: WslcProcessSettings,
_working_dir: Option<CString>,
_cmd_args: Vec<CString>,
_cmd_ptrs: Vec<*const i8>,
_env_vars: Vec<CString>,
_env_ptrs: Vec<*const i8>,
}
#[derive(Debug)]
enum ProcessCallbackMode {
None,
Registered {
callbacks: WslcProcessCallbacks,
context: usize,
},
}
#[derive(Debug)]
pub struct ProcessBuilder {
working_directory: Option<String>,
cmd_line: Vec<String>,
env_variables: Vec<String>,
flags: WslcProcessFlags,
callback_mode: ProcessCallbackMode,
stream_state: Option<Arc<StreamState>>,
}
impl Default for ProcessBuilder {
fn default() -> Self {
Self::new()
}
}
impl ProcessBuilder {
pub fn new() -> Self {
Self {
working_directory: None,
cmd_line: Vec::new(),
env_variables: Vec::new(),
flags: 0,
callback_mode: ProcessCallbackMode::None,
stream_state: None,
}
}
pub fn working_directory(mut self, dir: impl Into<String>) -> Self {
self.working_directory = Some(dir.into());
self
}
pub fn command(mut self, args: &[impl AsRef<str>]) -> Self {
self.cmd_line = args.iter().map(|s| s.as_ref().to_string()).collect();
self
}
pub fn env(mut self, key: &str, value: &str) -> Self {
self.env_variables.push(format!("{key}={value}"));
self
}
pub fn enable_stdin(mut self, enable: bool) -> Self {
if enable {
self.flags |= WSLC_PROCESS_FLAG_STDIN;
} else {
self.flags &= !WSLC_PROCESS_FLAG_STDIN;
}
self
}
pub unsafe fn callbacks(
mut self,
callbacks: WslcProcessCallbacks,
context: *mut c_void,
) -> Self {
self.callback_mode = ProcessCallbackMode::Registered {
callbacks,
context: context as usize,
};
self
}
pub fn with_streaming_io(self) -> (Self, ProcessStreams) {
self.with_streaming_io_capacity(64)
}
pub fn with_streaming_io_capacity(mut self, capacity: usize) -> (Self, ProcessStreams) {
let (callbacks, stream_id, state, streams) = setup_streaming_channels(capacity);
self.callback_mode = ProcessCallbackMode::Registered {
callbacks,
context: stream_id,
};
self.stream_state = Some(state);
(self, streams)
}
pub(crate) fn stream_state(&self) -> Option<Arc<StreamState>> {
self.stream_state.clone()
}
fn apply_cmd_line(
&self,
settings: &mut WslcProcessSettings,
) -> Result<(Vec<CString>, Vec<*const i8>), WslcError> {
if self.cmd_line.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let mut c_args = Vec::with_capacity(self.cmd_line.len());
for arg in &self.cmd_line {
let c_str = CString::new(arg.as_str())
.map_err(|e| WslcError::NulError(format!("命令参数含非法空字符: {e}")))?;
c_args.push(c_str);
}
let ptrs: Vec<*const i8> = c_args.iter().map(|c| c.as_ptr()).collect();
let hr = unsafe { WslcSetProcessSettingsCmdLine(settings, ptrs.as_ptr(), ptrs.len()) };
WslcError::check_hr(hr, "设置进程命令行参数失败")?;
Ok((c_args, ptrs))
}
fn apply_env_variables(
&self,
settings: &mut WslcProcessSettings,
) -> Result<(Vec<CString>, Vec<*const i8>), WslcError> {
if self.env_variables.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let mut c_envs = Vec::with_capacity(self.env_variables.len());
for env in &self.env_variables {
let c_str = CString::new(env.as_str())
.map_err(|e| WslcError::NulError(format!("环境变量含非法空字符: {e}")))?;
c_envs.push(c_str);
}
let ptrs: Vec<*const i8> = c_envs.iter().map(|c| c.as_ptr()).collect();
let hr = unsafe { WslcSetProcessSettingsEnvVariables(settings, ptrs.as_ptr(), ptrs.len()) };
WslcError::check_hr(hr, "设置进程环境变量失败")?;
Ok((c_envs, ptrs))
}
pub(crate) fn build_raw_settings(self) -> Result<RetainedProcessSettings, WslcError> {
let mut settings = WslcProcessSettings::default();
let hr = unsafe { WslcInitProcessSettings(&mut settings) };
WslcError::check_hr(hr, "初始化进程配置失败")?;
let working_dir = if let Some(ref dir) = self.working_directory {
let c_dir = CString::new(dir.as_str())
.map_err(|e| WslcError::NulError(format!("工作目录含非法空字符: {e}")))?;
let hr =
unsafe { WslcSetProcessSettingsWorkingDirectory(&mut settings, c_dir.as_ptr()) };
WslcError::check_hr(hr, "设置进程工作目录失败")?;
Some(c_dir)
} else {
None
};
let (cmd_args, cmd_ptrs) = self.apply_cmd_line(&mut settings)?;
let (env_vars, env_ptrs) = self.apply_env_variables(&mut settings)?;
if self.flags != 0 {
let hr = unsafe { WslcSetProcessSettingsFlags(&mut settings, self.flags) };
WslcError::check_hr(hr, "设置进程标志位失败")?;
}
if let ProcessCallbackMode::Registered { callbacks, context } = self.callback_mode {
let hr = unsafe {
WslcSetProcessSettingsCallbacks(&mut settings, &callbacks, context as *mut c_void)
};
WslcError::check_hr(hr, "设置进程回调函数失败")?;
}
Ok(RetainedProcessSettings {
settings,
_working_dir: working_dir,
_cmd_args: cmd_args,
_cmd_ptrs: cmd_ptrs,
_env_vars: env_vars,
_env_ptrs: env_ptrs,
})
}
pub fn spawn(self, container: &WslcContainerHandle) -> Result<WslcProcessHandle, WslcError> {
let stream_state = self.stream_state.clone();
let cmd_repr = format!("{:?}", self.cmd_line);
let _com_guard = crate::com::try_initialize_mta()?;
let mut retained = self.build_raw_settings()?;
let mut raw_process = WslcProcess::NULL;
let mut err_msg: *mut u16 = std::ptr::null_mut();
let hr = unsafe {
WslcCreateContainerProcess(
container.as_raw(),
&mut retained.settings,
&mut raw_process,
&mut err_msg,
)
};
unsafe {
WslcError::check(hr, err_msg)?;
}
if raw_process.is_null() {
return Err(WslcError::InvalidHandle);
}
log::info!("容器内新进程派生成功,命令行: {}", cmd_repr);
Ok(WslcProcessHandle::from_raw_with_stream(
raw_process,
container.clone(),
stream_state,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::channel::AsyncRecvError;
#[test]
fn test_streaming_io_establishes_two_bounded_channels() {
let builder = ProcessBuilder::new().command(&["/bin/sh", "-c", "echo hello"]);
let mut streams = builder.with_streaming_io().1;
assert!(matches!(
streams.stdout.try_recv(),
Err(AsyncRecvError::Empty)
));
assert!(matches!(
streams.stderr.try_recv(),
Err(AsyncRecvError::Empty)
));
assert_eq!(streams.stdout_dropped_bytes(), 0);
assert_eq!(streams.stderr_dropped_bytes(), 0);
}
#[test]
fn test_streaming_io_early_stream_drop_does_not_break_builder() {
let builder = ProcessBuilder::new().command(&["/bin/sh", "-c", "echo hello"]);
let (builder, streams) = builder.with_streaming_io();
drop(streams);
assert!(builder.build_raw_settings().is_ok());
}
#[test]
fn test_builder_chaining_completes_without_error() {
let builder = ProcessBuilder::new()
.command(&["/bin/env"])
.working_directory("/tmp")
.env("A", "1")
.env("B", "2")
.enable_stdin(true)
.enable_stdin(false);
assert!(builder.build_raw_settings().is_ok());
}
#[test]
fn test_nul_byte_in_arguments_is_rejected() {
let builder = ProcessBuilder::new().command(&["/bin/sh", "-c", "bad\0arg"]);
match builder.build_raw_settings() {
Err(WslcError::NulError(_)) => {}
Ok(_) => panic!("预期返回 NulError,实际却构建成功"),
Err(other) => panic!("预期返回 NulError,实际为: {other}"),
}
}
}