#![allow(clippy::undocumented_unsafe_blocks)]
use crate::capability::SecurityCapabilities;
use crate::{AcError, Result};
#[cfg(windows)]
use crate::ffi::attr_list::AttrList as FAttrList;
#[cfg(windows)]
use crate::ffi::handles::Handle as FHandle;
#[cfg(windows)]
use crate::ffi::sec_caps::OwnedSecurityCapabilities;
#[cfg(windows)]
use crate::ffi::sid::OwnedSid;
use std::ffi::OsString;
#[cfg(windows)]
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, TRUE};
#[cfg(windows)]
use windows::Win32::Foundation::{HANDLE_FLAG_INHERIT, SetHandleInformation};
#[cfg(windows)]
use windows::Win32::Security::Authorization::ConvertStringSidToSidW;
#[cfg(all(windows, feature = "tracing"))]
use windows::Win32::Security::SECURITY_CAPABILITIES;
#[cfg(windows)]
use windows::Win32::Security::{PSID, SECURITY_ATTRIBUTES};
#[cfg(windows)]
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ,
FILE_SHARE_WRITE, OPEN_EXISTING,
};
#[cfg(windows)]
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_CPU_RATE_CONTROL_ENABLE,
JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_LIMIT_PROCESS_MEMORY, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectCpuRateControlInformation,
JobObjectExtendedLimitInformation, SetInformationJobObject,
};
#[cfg(windows)]
use windows::Win32::System::Pipes::CreatePipe;
#[cfg(windows)]
use windows::Win32::System::Threading::{
CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT, CreateProcessW, EXTENDED_STARTUPINFO_PRESENT,
PROCESS_INFORMATION, STARTUPINFOEXW, STARTUPINFOW,
};
#[cfg(windows)]
use windows::Win32::System::WindowsProgramming::PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT;
#[cfg(windows)]
use windows::core::{PCWSTR, PWSTR};
#[derive(Clone, Copy, Debug)]
pub enum StdioConfig {
Inherit,
Null,
Pipe,
}
#[derive(Clone, Debug, Default)]
pub struct JobLimits {
pub memory_bytes: Option<usize>,
pub cpu_rate_percent: Option<u32>,
pub kill_on_job_close: bool,
}
#[derive(Clone, Debug)]
pub struct LaunchOptions {
pub exe: std::path::PathBuf,
pub cmdline: Option<String>,
pub cwd: Option<std::path::PathBuf>,
pub env: Option<Vec<(std::ffi::OsString, std::ffi::OsString)>>,
pub stdio: StdioConfig,
pub suspended: bool,
pub join_job: Option<JobLimits>,
pub startup_timeout: Option<std::time::Duration>,
}
impl Default for LaunchOptions {
fn default() -> Self {
#[cfg(target_os = "windows")]
let cwd = Some(std::path::PathBuf::from("C:\\Windows\\System32"));
#[cfg(not(target_os = "windows"))]
let cwd = None;
Self {
exe: std::path::PathBuf::from("C:\\Windows\\System32\\notepad.exe"),
cmdline: None,
cwd,
env: None,
stdio: StdioConfig::Inherit,
suspended: false,
join_job: None,
startup_timeout: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Launched {
pub pid: u32,
}
#[cfg(windows)]
#[derive(Debug)]
pub struct LaunchedIo {
pub pid: u32,
pub stdin: Option<std::fs::File>,
pub stdout: Option<std::fs::File>,
pub stderr: Option<std::fs::File>,
pub job_guard: Option<JobGuard>,
pub(crate) process: FHandle,
}
#[cfg(not(windows))]
pub struct LaunchedIo;
#[cfg(windows)]
#[derive(Debug)]
pub struct JobGuard(FHandle);
#[cfg(windows)]
impl JobGuard {
pub fn as_handle(&self) -> HANDLE {
self.0.as_win32()
}
}
#[cfg(windows)]
#[derive(Debug)]
pub struct JobObjectDropGuard {
handle: FHandle,
kill_on_drop: bool,
}
#[cfg(windows)]
impl JobObjectDropGuard {
pub fn new() -> Result<Self> {
use windows::Win32::System::JobObjects::{
CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
use windows::core::PCWSTR;
let hjob = unsafe {
CreateJobObjectW(None, PCWSTR::null())
.map_err(|e| AcError::Win32(format!("CreateJobObjectW failed: {e}")))?
};
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
unsafe {
SetInformationJobObject(
hjob,
JobObjectExtendedLimitInformation,
&info as *const _ as *const _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|_| AcError::Win32("SetInformationJobObject(kill_on_close) failed".into()))?;
}
Ok(Self {
handle: unsafe { FHandle::from_raw(hjob.0 as *mut _) }
.map_err(|_| AcError::Win32("invalid job handle".into()))?,
kill_on_drop: true,
})
}
pub fn as_handle(&self) -> HANDLE {
self.handle.as_win32()
}
pub fn assign_process_handle(&self, process: HANDLE) -> Result<()> {
use windows::Win32::System::JobObjects::AssignProcessToJobObject;
unsafe {
AssignProcessToJobObject(self.handle.as_win32(), process)
.map_err(|_| AcError::Win32("AssignProcessToJobObject failed".into()))
}
}
pub fn disable_kill_on_drop(&mut self) -> Result<()> {
use windows::Win32::System::JobObjects::{
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
let info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
unsafe {
SetInformationJobObject(
self.handle.as_win32(),
JobObjectExtendedLimitInformation,
&info as *const _ as *const _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|_| AcError::Win32("SetInformationJobObject(clear) failed".into()))?;
}
self.kill_on_drop = false;
Ok(())
}
}
pub fn launch_in_container(_sec: &SecurityCapabilities, _opts: &LaunchOptions) -> Result<Launched> {
#[cfg(windows)]
{
unsafe { launch_impl(_sec, _opts).map(|io| Launched { pid: io.pid }) }
}
#[cfg(not(windows))]
{
Err(AcError::UnsupportedPlatform)
}
}
#[cfg(windows)]
fn build_env_block(env: &[(std::ffi::OsString, std::ffi::OsString)]) -> Vec<u16> {
let mut block: Vec<u16> = Vec::new();
for (k, v) in env {
let mut kv = std::ffi::OsString::from(k);
kv.push("=");
kv.push(v);
let mut w: Vec<u16> =
std::os::windows::ffi::OsStrExt::encode_wide(kv.as_os_str()).collect();
w.push(0);
block.extend_from_slice(&w);
}
block.push(0);
block
}
pub fn merge_parent_env(mut custom: Vec<(OsString, OsString)>) -> Vec<(OsString, OsString)> {
const KEYS: &[&str] = &[
"SystemRoot",
"windir",
"ComSpec",
"PATHEXT",
"TEMP",
"TMP",
"PATH",
];
for key in KEYS {
if std::env::var_os(key).is_some()
&& !custom.iter().any(|(k, _)| k == key)
&& let Some(val) = std::env::var_os(key)
{
custom.push((OsString::from(key), val));
}
}
custom
}
#[cfg(windows)]
struct AttributeContext {
attr_list: FAttrList,
_sc_owned: Box<OwnedSecurityCapabilities>,
_handle_list: Option<Vec<HANDLE>>,
_lpac_policy: Option<Box<u32>>,
}
#[cfg(windows)]
impl AttributeContext {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn new(sec: &SecurityCapabilities, handle_list: Option<Vec<HANDLE>>) -> Result<Self> {
#[cfg(feature = "tracing")]
tracing::trace!(
"setup_attributes: lpac={}, caps_named_count={}, stdio_handles={}",
sec.lpac,
sec.caps.len(),
handle_list.as_ref().map(|s| s.len()).unwrap_or(0)
);
#[cfg(feature = "tracing")]
if sec.caps.is_empty() {
tracing::trace!("setup_attributes: pure AppContainer (no capabilities)");
} else {
for cap in &sec.caps {
tracing::trace!("setup_attributes: capability SDDL={}", cap.sid_sddl);
}
}
let pkg_w: Vec<u16> = crate::util::to_utf16(sec.package.as_string());
let mut pkg_psid_raw = PSID(std::ptr::null_mut());
if ConvertStringSidToSidW(PCWSTR(pkg_w.as_ptr()), &mut pkg_psid_raw).is_err() {
return Err(AcError::LaunchFailed {
stage: "ConvertStringSidToSidW(package)",
hint: "invalid package SID",
source: Box::new(std::io::Error::last_os_error()),
});
}
let pkg_owned = OwnedSid::from_localfree_psid(pkg_psid_raw.0);
let mut caps_owned: Vec<OwnedSid> = Vec::with_capacity(sec.caps.len());
for cap in &sec.caps {
let sddl_w: Vec<u16> = crate::util::to_utf16(&cap.sid_sddl);
let mut psid_raw = PSID(std::ptr::null_mut());
if ConvertStringSidToSidW(PCWSTR(sddl_w.as_ptr()), &mut psid_raw).is_err() {
return Err(AcError::LaunchFailed {
stage: "ConvertStringSidToSidW(capability)",
hint: "invalid capability SID",
source: Box::new(std::io::Error::last_os_error()),
});
}
caps_owned.push(OwnedSid::from_localfree_psid(psid_raw.0));
}
let sc_owned = Box::new(OwnedSecurityCapabilities::new(pkg_owned, caps_owned));
let mut attr_count = 1;
if sec.lpac {
attr_count += 1;
}
if handle_list.is_some() {
attr_count += 1;
}
#[cfg(feature = "tracing")]
tracing::debug!("AttrList: count={}", attr_count);
let mut attr_list = FAttrList::with_capacity(attr_count as u32)?;
let mut si_ex: STARTUPINFOEXW = std::mem::zeroed();
si_ex.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
si_ex.lpAttributeList = attr_list.as_mut_ptr();
attr_list.set_security_capabilities(&sc_owned)?;
#[cfg(feature = "tracing")]
tracing::trace!(
"UpdateProcThreadAttribute(security): attr_list_ptr={:p}, value_ptr={:p}, value_size={}",
si_ex.lpAttributeList.0,
sc_owned.as_ptr(),
std::mem::size_of::<SECURITY_CAPABILITIES>()
);
let mut lpac_policy: Option<Box<u32>> = None;
if sec.lpac {
lpac_policy = Some(Box::new(PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT));
let p = lpac_policy.as_ref().unwrap();
attr_list.set_all_app_packages_policy(p)?;
#[cfg(feature = "tracing")]
tracing::trace!(
"UpdateProcThreadAttribute(AAPolicy via wrapper): attr_list_ptr={:p}, policy_ptr={:p}, size={}",
si_ex.lpAttributeList.0,
&**p as *const u32,
std::mem::size_of::<u32>()
);
}
if let Some(ref handles) = handle_list {
attr_list.set_handle_list(handles)?;
#[cfg(feature = "tracing")]
{
tracing::trace!(
"UpdateProcThreadAttribute(handles via wrapper): attr_list_ptr={:p}, count={}, bytes={}",
si_ex.lpAttributeList.0,
handles.len(),
std::mem::size_of::<HANDLE>() * handles.len()
);
for (idx, handle) in handles.iter().enumerate() {
tracing::trace!("inherit_handle[{}]=0x{:X}", idx, handle.0 as usize);
}
}
}
Ok(Self {
attr_list,
_sc_owned: sc_owned,
_handle_list: handle_list,
_lpac_policy: lpac_policy,
})
}
fn as_mut_ptr(&mut self) -> windows::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST {
self.attr_list.as_mut_ptr()
}
}
#[cfg(windows)]
impl Drop for AttributeContext {
fn drop(&mut self) {
}
}
#[cfg(windows)]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn make_cmd_args(cmdline: &Option<String>) -> Option<Vec<u16>> {
cmdline.as_ref().map(|cl| {
let mut w: Vec<u16> = cl.encode_utf16().collect();
w.push(0);
w
})
}
#[cfg(windows)]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn launch_impl(sec: &SecurityCapabilities, opts: &LaunchOptions) -> Result<LaunchedIo> {
if sec.lpac {
crate::supports_lpac()?;
}
let force_env = std::env::var_os("RAPPCT_TEST_FORCE_ENV").is_some();
let env_block = if let Some(e) = opts.env.as_ref() {
Some(build_env_block(e))
} else if force_env {
let mut all: Vec<(OsString, OsString)> = std::env::vars_os().collect();
all.sort_by(|a, b| {
a.0.to_string_lossy()
.to_ascii_lowercase()
.cmp(&b.0.to_string_lossy().to_ascii_lowercase())
});
Some(build_env_block(&all))
} else {
None
};
let exe_w: Vec<u16> = crate::util::to_utf16_os(opts.exe.as_os_str());
let mut args_w = make_cmd_args(&opts.cmdline);
let mut cwd_w = opts
.cwd
.as_ref()
.map(|p| crate::util::to_utf16_os(p.as_os_str()));
if std::env::var_os("RAPPCT_TEST_NO_CWD").is_some() {
cwd_w = None;
}
let mut si_ex: STARTUPINFOEXW = std::mem::zeroed();
si_ex.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
let mut child_stdin = HANDLE::default();
let mut child_stdout = HANDLE::default();
let mut child_stderr = HANDLE::default();
let mut parent_stdin: Option<FHandle> = None;
let mut parent_stdout: Option<FHandle> = None;
let mut parent_stderr: Option<FHandle> = None;
let mut inherit_handles = false;
match opts.stdio {
StdioConfig::Inherit => {}
StdioConfig::Null => {
let mut sa: SECURITY_ATTRIBUTES = std::mem::zeroed();
sa.nLength = std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32;
sa.bInheritHandle = TRUE;
let nul: Vec<u16> = crate::util::to_utf16("NUL");
let h_in = CreateFileW(
PCWSTR(nul.as_ptr()),
FILE_GENERIC_READ.0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
Some(&sa as *const _),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
None,
)
.map_err(|_| AcError::LaunchFailed {
stage: "CreateFileW(NUL)",
hint: "stdin",
source: Box::new(std::io::Error::last_os_error()),
})?;
let h_out = CreateFileW(
PCWSTR(nul.as_ptr()),
FILE_GENERIC_WRITE.0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
Some(&sa as *const _),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
None,
)
.map_err(|_| AcError::LaunchFailed {
stage: "CreateFileW(NUL)",
hint: "stdout",
source: Box::new(std::io::Error::last_os_error()),
})?;
let h_err = CreateFileW(
PCWSTR(nul.as_ptr()),
FILE_GENERIC_WRITE.0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
Some(&sa as *const _),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
None,
)
.map_err(|_| AcError::LaunchFailed {
stage: "CreateFileW(NUL)",
hint: "stderr",
source: Box::new(std::io::Error::last_os_error()),
})?;
si_ex.StartupInfo.hStdInput = h_in;
si_ex.StartupInfo.hStdOutput = h_out;
si_ex.StartupInfo.hStdError = h_err;
si_ex.StartupInfo.dwFlags |= windows::Win32::System::Threading::STARTF_USESTDHANDLES;
inherit_handles = true;
}
StdioConfig::Pipe => {
let mut sa: SECURITY_ATTRIBUTES = std::mem::zeroed();
sa.nLength = std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32;
sa.bInheritHandle = TRUE;
let (mut r_in, mut w_in) = (HANDLE::default(), HANDLE::default());
CreatePipe(&mut r_in, &mut w_in, Some(&sa), 0).map_err(|_| AcError::LaunchFailed {
stage: "CreatePipe(stdin)",
hint: "pipe",
source: Box::new(std::io::Error::last_os_error()),
})?;
let (mut r_out, mut w_out) = (HANDLE::default(), HANDLE::default());
CreatePipe(&mut r_out, &mut w_out, Some(&sa), 0).map_err(|_| {
AcError::LaunchFailed {
stage: "CreatePipe(stdout)",
hint: "pipe",
source: Box::new(std::io::Error::last_os_error()),
}
})?;
let (mut r_err, mut w_err) = (HANDLE::default(), HANDLE::default());
CreatePipe(&mut r_err, &mut w_err, Some(&sa), 0).map_err(|_| {
AcError::LaunchFailed {
stage: "CreatePipe(stderr)",
hint: "pipe",
source: Box::new(std::io::Error::last_os_error()),
}
})?;
let _ = SetHandleInformation(
w_in,
HANDLE_FLAG_INHERIT.0,
windows::Win32::Foundation::HANDLE_FLAGS(0),
);
let _ = SetHandleInformation(
r_out,
HANDLE_FLAG_INHERIT.0,
windows::Win32::Foundation::HANDLE_FLAGS(0),
);
let _ = SetHandleInformation(
r_err,
HANDLE_FLAG_INHERIT.0,
windows::Win32::Foundation::HANDLE_FLAGS(0),
);
si_ex.StartupInfo.hStdInput = r_in; si_ex.StartupInfo.hStdOutput = w_out; si_ex.StartupInfo.hStdError = w_err; si_ex.StartupInfo.dwFlags |= windows::Win32::System::Threading::STARTF_USESTDHANDLES;
inherit_handles = true;
child_stdin = r_in;
child_stdout = w_out;
child_stderr = w_err;
parent_stdin = Some(
unsafe { FHandle::from_raw(w_in.0 as *mut _) }
.map_err(|_| AcError::Win32("invalid stdin handle".into()))?,
);
parent_stdout = Some(
unsafe { FHandle::from_raw(r_out.0 as *mut _) }
.map_err(|_| AcError::Win32("invalid stdout handle".into()))?,
);
parent_stderr = Some(
unsafe { FHandle::from_raw(r_err.0 as *mut _) }
.map_err(|_| AcError::Win32("invalid stderr handle".into()))?,
);
}
}
let handles_for_attr: Option<Vec<HANDLE>> =
if inherit_handles && matches!(opts.stdio, StdioConfig::Pipe) {
Some(vec![child_stdin, child_stdout, child_stderr])
} else {
None
};
let mut attr_ctx = AttributeContext::new(sec, handles_for_attr)?;
si_ex.lpAttributeList = attr_ctx.as_mut_ptr();
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
let mut flags = EXTENDED_STARTUPINFO_PRESENT;
if env_block.is_some() {
flags |= CREATE_UNICODE_ENVIRONMENT;
}
if opts.suspended {
flags |= CREATE_SUSPENDED;
}
#[cfg(feature = "tracing")]
{
let inherit_handles_dbg = inherit_handles;
let env_bytes = env_block.as_ref().map(|b| b.len()).unwrap_or(0);
tracing::trace!(
"CreateProcessW: exe={:?}, args_present={}, cwd_present={}, lpAttributeList={:p}, inherit_handles={}, flags=0x{:X}, env_bytes={}",
opts.exe,
args_w.as_ref().map(|v| v.len()).is_some(),
cwd_w.as_ref().is_some(),
si_ex.lpAttributeList.0,
inherit_handles_dbg,
flags.0,
env_bytes
);
}
let cp_res = CreateProcessW(
PCWSTR(exe_w.as_ptr()),
args_w.as_mut().map(|v| PWSTR(v.as_mut_ptr())),
None,
None,
inherit_handles,
flags,
env_block
.as_ref()
.map(|b| b.as_ptr() as *const std::ffi::c_void),
cwd_w
.as_ref()
.map(|v| PCWSTR(v.as_ptr()))
.unwrap_or(PCWSTR::null()),
&mut si_ex as *mut STARTUPINFOEXW as *mut STARTUPINFOW,
&mut pi,
);
let ok = cp_res.is_ok();
if !ok {
#[cfg(feature = "tracing")]
{
use windows::Win32::Foundation::GetLastError;
let gle = unsafe { GetLastError().0 };
let (hr, msg) = match &cp_res {
Ok(_) => (0, String::new()),
Err(e) => (e.code().0, format!("{}", e)),
};
tracing::error!(
"CreateProcessW failed: GLE={}, hr=0x{:X}, msg={}",
gle,
hr,
msg
);
}
if std::env::var_os("RAPPCT_DEBUG_LAUNCH").is_some() {
use windows::Win32::Foundation::GetLastError;
let gle = unsafe { GetLastError().0 };
let (hr, msg) = match &cp_res {
Ok(_) => (0, String::new()),
Err(e) => (e.code().0, format!("{}", e)),
};
eprintln!(
"[rappct] CreateProcessW failed: GLE={} hr=0x{:X} msg={} exe={:?} args_present={} cwd_present={} inherit_handles={} flags=0x{:X}",
gle,
hr,
msg,
opts.exe,
args_w.as_ref().map(|v| v.len()).is_some(),
cwd_w.as_ref().is_some(),
inherit_handles,
flags.0,
);
}
if inherit_handles {
if child_stdin != INVALID_HANDLE_VALUE {
let _ = CloseHandle(child_stdin);
}
if child_stdout != INVALID_HANDLE_VALUE {
let _ = CloseHandle(child_stdout);
}
if child_stderr != INVALID_HANDLE_VALUE {
let _ = CloseHandle(child_stderr);
}
}
return Err(AcError::LaunchFailed {
stage: "CreateProcessW",
hint: "extended startup with AC/LPAC",
source: Box::new(std::io::Error::last_os_error()),
});
}
drop(attr_ctx);
if inherit_handles {
if child_stdin != INVALID_HANDLE_VALUE {
let _ = CloseHandle(child_stdin);
}
if child_stdout != INVALID_HANDLE_VALUE {
let _ = CloseHandle(child_stdout);
}
if child_stderr != INVALID_HANDLE_VALUE {
let _ = CloseHandle(child_stderr);
}
}
let mut job_guard: Option<JobGuard> = None;
if let Some(limits) = &opts.join_job {
let hjob = CreateJobObjectW(None, PCWSTR::null())
.map_err(|e| AcError::Win32(format!("CreateJobObjectW failed: {e}")))?;
if limits.memory_bytes.is_some() || limits.kill_on_job_close {
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
if let Some(bytes) = limits.memory_bytes {
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY;
info.ProcessMemoryLimit = bytes;
}
if limits.kill_on_job_close {
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
}
SetInformationJobObject(
hjob,
JobObjectExtendedLimitInformation,
&info as *const _ as *const _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|_| AcError::LaunchFailed {
stage: "SetInformationJobObject(ext)",
hint: "set job limits",
source: Box::new(std::io::Error::last_os_error()),
})?;
}
if let Some(percent) = limits.cpu_rate_percent {
let mut info: JOBOBJECT_CPU_RATE_CONTROL_INFORMATION = std::mem::zeroed();
info.ControlFlags =
JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP;
info.Anonymous.CpuRate = percent.clamp(1, 100) * 100;
SetInformationJobObject(
hjob,
JobObjectCpuRateControlInformation,
&info as *const _ as *const _,
std::mem::size_of::<JOBOBJECT_CPU_RATE_CONTROL_INFORMATION>() as u32,
)
.map_err(|_| AcError::LaunchFailed {
stage: "SetInformationJobObject(cpu)",
hint: "set cpu cap",
source: Box::new(std::io::Error::last_os_error()),
})?;
}
AssignProcessToJobObject(hjob, pi.hProcess).map_err(|_| AcError::LaunchFailed {
stage: "AssignProcessToJobObject",
hint: "attach child",
source: Box::new(std::io::Error::last_os_error()),
})?;
if limits.kill_on_job_close {
job_guard = Some(JobGuard(
unsafe { FHandle::from_raw(hjob.0 as *mut _) }
.map_err(|_| AcError::Win32("invalid job handle".into()))?,
));
} else {
let _ = CloseHandle(hjob);
}
}
let _ = CloseHandle(pi.hThread);
let proc_handle = unsafe { FHandle::from_raw(pi.hProcess.0 as *mut _) }
.map_err(|_| AcError::Win32("invalid process handle".into()))?;
Ok(LaunchedIo {
pid: pi.dwProcessId,
stdin: parent_stdin.map(|h| h.into_file()),
stdout: parent_stdout.map(|h| h.into_file()),
stderr: parent_stderr.map(|h| h.into_file()),
job_guard,
process: proc_handle,
})
}
#[cfg(windows)]
pub fn launch_in_container_with_io(
sec: &SecurityCapabilities,
opts: &LaunchOptions,
) -> Result<LaunchedIo> {
unsafe { launch_impl(sec, opts) }
}
#[cfg(windows)]
impl LaunchedIo {
pub fn wait(self, timeout: Option<std::time::Duration>) -> Result<u32> {
use windows::Win32::Foundation::{STILL_ACTIVE, WAIT_FAILED, WAIT_TIMEOUT};
use windows::Win32::System::Threading::{
GetExitCodeProcess, INFINITE, WaitForSingleObject,
};
unsafe {
let ms = timeout
.map(|d| d.as_millis().min(u128::from(u32::MAX)) as u32)
.unwrap_or(INFINITE);
let r = WaitForSingleObject(self.process.as_win32(), ms);
if r == WAIT_FAILED {
return Err(AcError::Win32("WaitForSingleObject failed".into()));
}
if r == WAIT_TIMEOUT {
return Err(AcError::LaunchFailed {
stage: "wait",
hint: "timeout",
source: Box::new(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"wait timeout",
)),
});
}
let mut code: u32 = STILL_ACTIVE.0 as u32;
GetExitCodeProcess(self.process.as_win32(), &mut code)
.map_err(|_| AcError::Win32("GetExitCodeProcess failed".into()))?;
Ok(code)
}
}
}
#[cfg(not(windows))]
pub fn launch_in_container_with_io(
_sec: &SecurityCapabilities,
_opts: &LaunchOptions,
) -> Result<LaunchedIo> {
Err(AcError::UnsupportedPlatform)
}
#[cfg(test)]
mod tests {
use super::merge_parent_env;
use std::ffi::OsString;
#[test]
fn merge_parent_env_includes_essentials_if_present() {
let out = merge_parent_env(vec![(OsString::from("RAPPCT_X"), OsString::from("1"))]);
assert!(out.iter().any(|(k, v)| k == "RAPPCT_X" && v == "1"));
if std::env::var_os("SystemRoot").is_some() {
assert!(out.iter().any(|(k, _)| k == "SystemRoot"));
}
}
}