use std::collections::BTreeMap;
use std::ffi::{OsStr, c_void};
use std::os::windows::ffi::OsStrExt;
use std::os::windows::process::ExitStatusExt;
use std::path::Path;
use std::process::ExitStatus;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
SetInformationJobObject,
};
use windows_sys::Win32::System::Threading::{
CreateProcessW, GetExitCodeProcess, PROCESS_INFORMATION, STARTUPINFOW, TerminateProcess,
WaitForSingleObject,
};
use crate::{Error, Result};
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const OPEN_EXISTING: u32 = 3;
const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
const CREATE_UNICODE_ENVIRONMENT: u32 = 0x0000_0400;
const STARTF_USESTDHANDLES: u32 = 0x0000_0100;
const INFINITE: u32 = 0xFFFF_FFFF;
const FOPEN: u8 = 0x01;
const FPIPE: u8 = 0x08;
const FDEV: u8 = 0x40;
const JOB_OBJECT_EXTENDED_LIMIT_INFO_CLASS: i32 = 9;
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: u32 = 0x0000_2000;
pub(crate) struct JobHandle(HANDLE);
unsafe impl Send for JobHandle {}
unsafe impl Sync for JobHandle {}
impl JobHandle {
pub(crate) fn create_for(process: HANDLE) -> Option<JobHandle> {
if process.is_null() {
return None;
}
unsafe {
let job = CreateJobObjectW(core::ptr::null(), core::ptr::null());
if job.is_null() {
return None;
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = core::mem::zeroed();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let set_ok = SetInformationJobObject(
job,
JOB_OBJECT_EXTENDED_LIMIT_INFO_CLASS,
&info as *const _ as *const c_void,
core::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
);
if set_ok == 0 || AssignProcessToJobObject(job, process) == 0 {
CloseHandle(job);
return None;
}
Some(JobHandle(job))
}
}
}
impl Drop for JobHandle {
fn drop(&mut self) {
unsafe {
if !self.0.is_null() {
CloseHandle(self.0);
}
}
}
}
pub struct Spawned {
pub child: WinChild,
pub writer: NamedPipeServer,
pub reader: NamedPipeServer,
pub stdout: NamedPipeServer,
pub stderr: NamedPipeServer,
}
pub struct WinChild {
process: HANDLE,
thread: HANDLE,
#[allow(dead_code)]
pid: u32,
exit_code: Option<u32>,
job: Option<JobHandle>,
}
unsafe impl Send for WinChild {}
unsafe impl Sync for WinChild {}
impl WinChild {
pub async fn wait(&mut self) -> std::io::Result<ExitStatus> {
if let Some(code) = self.exit_code {
return Ok(ExitStatus::from_raw(code));
}
let h = SendHandle(self.process);
let code = tokio::task::spawn_blocking(move || {
let h = h;
unsafe {
WaitForSingleObject(h.0, INFINITE);
let mut code: u32 = 0;
GetExitCodeProcess(h.0, &mut code);
code
}
})
.await
.map_err(std::io::Error::other)?;
self.exit_code = Some(code);
Ok(ExitStatus::from_raw(code))
}
pub fn start_kill(&mut self) -> std::io::Result<()> {
let _ = self.job.take();
if self.exit_code.is_none() {
unsafe { TerminateProcess(self.process, 1) };
}
Ok(())
}
pub async fn kill(&mut self) -> std::io::Result<()> {
self.start_kill()?;
self.wait().await.map(|_| ())
}
}
impl Drop for WinChild {
fn drop(&mut self) {
let _ = self.job.take();
unsafe {
if self.exit_code.is_none() {
TerminateProcess(self.process, 1);
}
if !self.process.is_null() {
CloseHandle(self.process);
}
if !self.thread.is_null() {
CloseHandle(self.thread);
}
}
}
}
struct SendHandle(HANDLE);
unsafe impl Send for SendHandle {}
pub async fn spawn(program: &Path, args: &[String], envs: &[(String, String)]) -> Result<Spawned> {
let cmd = create_pipe("cmd", GENERIC_READ)?; let resp = create_pipe("resp", GENERIC_WRITE)?; let out = create_pipe("out", GENERIC_WRITE)?; let err = create_pipe("err", GENERIC_WRITE)?;
let nul = match open_inheritable("NUL", GENERIC_READ) {
Ok(h) => h,
Err(e) => {
close_all(&[cmd.client, resp.client, out.client, err.client]);
return Err(e);
}
};
let connect_res = async {
cmd.server.connect().await?;
resp.server.connect().await?;
out.server.connect().await?;
err.server.connect().await
}
.await;
if let Err(e) = connect_res {
close_all(&[nul, cmd.client, resp.client, out.client, err.client]);
return Err(Error::Transport(format!("命名管道连接失败: {e}")));
}
let mut block = build_crt_block(&[
(FOPEN | FDEV, nul), (FOPEN | FPIPE, out.client), (FOPEN | FPIPE, err.client), (FOPEN | FPIPE, cmd.client), (FOPEN | FPIPE, resp.client), ]);
let app = to_wide(&program.to_string_lossy());
let mut cmdline = build_command_line(program, args);
let env_block = build_env_block(envs);
let mut si: STARTUPINFOW = unsafe { core::mem::zeroed() };
si.cb = core::mem::size_of::<STARTUPINFOW>() as u32;
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = nul;
si.hStdOutput = out.client;
si.hStdError = err.client;
si.cbReserved2 = block.len() as u16;
si.lpReserved2 = block.as_mut_ptr();
let mut pi: PROCESS_INFORMATION = unsafe { core::mem::zeroed() };
let ok = unsafe {
CreateProcessW(
app.as_ptr(),
cmdline.as_mut_ptr(),
core::ptr::null(),
core::ptr::null(),
1, CREATE_UNICODE_ENVIRONMENT,
env_block.as_ptr() as *const c_void,
core::ptr::null(), &si,
&mut pi,
)
};
close_all(&[nul, cmd.client, resp.client, out.client, err.client]);
if ok == 0 {
let gle = unsafe { GetLastError() };
return Err(Error::Transport(format!(
"CreateProcessW 启动 Camoufox 失败(GetLastError={gle})"
)));
}
let job = JobHandle::create_for(pi.hProcess);
Ok(Spawned {
child: WinChild {
process: pi.hProcess,
thread: pi.hThread,
pid: pi.dwProcessId,
exit_code: None,
job,
},
writer: cmd.server,
reader: resp.server,
stdout: out.server,
stderr: err.server,
})
}
struct PipePair {
server: NamedPipeServer,
client: HANDLE,
}
fn create_pipe(tag: &str, client_access: u32) -> Result<PipePair> {
let name = unique_pipe_name(tag);
let server = ServerOptions::new()
.first_pipe_instance(true)
.create(&name)
.map_err(|e| Error::Transport(format!("创建命名管道 {tag} 失败: {e}")))?;
let client = open_inheritable(&name, client_access).map_err(|e| {
Error::Transport(format!("打开命名管道 {tag} 子端失败: {e}"))
})?;
Ok(PipePair { server, client })
}
fn open_inheritable(name: &str, access: u32) -> Result<HANDLE> {
let wide = to_wide(name);
let sa = SECURITY_ATTRIBUTES {
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: core::ptr::null_mut(),
bInheritHandle: 1, };
let h = unsafe {
CreateFileW(
wide.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
&sa,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, core::ptr::null_mut(),
)
};
if h == INVALID_HANDLE_VALUE {
let gle = unsafe { GetLastError() };
return Err(Error::Transport(format!(
"CreateFileW({name}) 失败(GetLastError={gle})"
)));
}
Ok(h)
}
fn unique_pipe_name(tag: &str) -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
format!(
r"\\.\pipe\drission-juggler-{}-{}-{}-{}",
std::process::id(),
nanos,
n,
tag
)
}
fn build_crt_block(entries: &[(u8, HANDLE)]) -> Vec<u8> {
let count = entries.len();
let mut buf = Vec::with_capacity(4 + count + count * core::mem::size_of::<HANDLE>());
buf.extend_from_slice(&(count as u32).to_ne_bytes());
for (flags, _) in entries {
buf.push(*flags);
}
for (_, h) in entries {
buf.extend_from_slice(&(*h as usize).to_ne_bytes());
}
buf
}
fn to_wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn build_command_line(program: &Path, args: &[String]) -> Vec<u16> {
let mut s = String::new();
append_arg(&mut s, &program.to_string_lossy());
for a in args {
s.push(' ');
append_arg(&mut s, a);
}
OsStr::new(&s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn append_arg(out: &mut String, arg: &str) {
let needs_quote = arg.is_empty() || arg.contains([' ', '\t', '"']);
if !needs_quote {
out.push_str(arg);
return;
}
out.push('"');
let mut backslashes = 0usize;
for c in arg.chars() {
match c {
'\\' => backslashes += 1,
'"' => {
for _ in 0..(backslashes * 2 + 1) {
out.push('\\');
}
out.push('"');
backslashes = 0;
}
_ => {
for _ in 0..backslashes {
out.push('\\');
}
out.push(c);
backslashes = 0;
}
}
}
for _ in 0..(backslashes * 2) {
out.push('\\');
}
out.push('"');
}
fn build_env_block(extra: &[(String, String)]) -> Vec<u16> {
let mut map: BTreeMap<String, (String, String)> = BTreeMap::new();
for (k, v) in std::env::vars() {
map.insert(k.to_uppercase(), (k, v));
}
for (k, v) in extra {
map.insert(k.to_uppercase(), (k.clone(), v.clone()));
}
let mut block: Vec<u16> = Vec::new();
for (_, (k, v)) in map {
let entry = format!("{k}={v}");
block.extend(OsStr::new(&entry).encode_wide());
block.push(0);
}
if block.is_empty() {
block.push(0);
}
block.push(0); block
}
fn close_all(handles: &[HANDLE]) {
for &h in handles {
if !h.is_null() && h != INVALID_HANDLE_VALUE {
unsafe { CloseHandle(h) };
}
}
}