use super::*;
use std::os::windows::{
ffi::OsStrExt,
io::{AsRawHandle, FromRawHandle},
};
use windows_sys::Win32::{
Foundation::{CloseHandle, HANDLE},
System::{Console::*, JobObjects::*, Pipes::*, Threading::*},
};
struct Handle(usize);
impl Handle {
fn new(raw: HANDLE) -> Result<Self, String> {
if raw.is_null() {
Err(error(std::io::Error::last_os_error()))
} else {
Ok(Self(raw as usize))
}
}
fn raw(&self) -> HANDLE {
self.0 as HANDLE
}
}
impl Drop for Handle {
fn drop(&mut self) {
unsafe {
CloseHandle(self.raw());
}
}
}
struct Console(HPCON);
impl Drop for Console {
fn drop(&mut self) {
unsafe {
ClosePseudoConsole(self.0);
}
}
}
struct Attributes(Vec<usize>);
impl Attributes {
fn new(console: HPCON) -> Result<Self, String> {
let mut bytes = 0;
unsafe {
InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut bytes);
}
if bytes == 0 {
return Err(error(std::io::Error::last_os_error()));
}
let mut memory = vec![0; bytes.div_ceil(std::mem::size_of::<usize>())];
let raw = memory.as_mut_ptr().cast();
if unsafe { InitializeProcThreadAttributeList(raw, 1, 0, &mut bytes) } == 0 {
return Err(error(std::io::Error::last_os_error()));
}
let mut value = Self(memory);
if unsafe {
UpdateProcThreadAttribute(
value.raw(),
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE as usize,
console as *const _,
std::mem::size_of::<HPCON>(),
std::ptr::null_mut(),
std::ptr::null(),
)
} == 0
{
return Err(error(std::io::Error::last_os_error()));
}
Ok(value)
}
fn raw(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
self.0.as_mut_ptr().cast()
}
}
impl Drop for Attributes {
fn drop(&mut self) {
unsafe {
DeleteProcThreadAttributeList(self.raw());
}
}
}
fn wide(path: &std::ffi::OsStr) -> Result<Vec<u16>, String> {
let mut units: Vec<_> = path.encode_wide().collect();
if units.contains(&0) {
return Err("path contains NUL".into());
}
units.push(0);
Ok(units)
}
fn pipe() -> Result<(File, File), String> {
let (mut read, mut write) = (std::ptr::null_mut(), std::ptr::null_mut());
if unsafe { CreatePipe(&mut read, &mut write, std::ptr::null(), 0) } == 0 {
return Err(error(std::io::Error::last_os_error()));
}
Ok(unsafe { (File::from_raw_handle(read), File::from_raw_handle(write)) })
}
struct Buffer {
data: Vec<u8>,
truncated: bool,
}
pub struct Session {
input: Option<super::terminal_input::InputPump>,
process: Handle,
job: Option<Handle>,
console: Option<Console>,
output: Arc<Mutex<Buffer>>,
stop: Arc<AtomicBool>,
reader: Option<std::thread::JoinHandle<()>>,
}
impl Session {
pub fn start(root: &Path, shell: &str) -> Result<Self, String> {
let shell = Path::new(shell);
if !shell.is_absolute() {
return Err("shell must be an absolute executable path".into());
}
let (read_input, input) = pipe()?;
let (mut output, write_output) = pipe()?;
let mut console = 0;
let status = unsafe {
CreatePseudoConsole(
COORD { X: 80, Y: 24 },
read_input.as_raw_handle(),
write_output.as_raw_handle(),
0,
&mut console,
)
};
if status < 0 {
return Err(format!("CreatePseudoConsole failed: {status:#x}"));
}
let console = Console(console);
drop(read_input);
drop(write_output);
let mut attributes = Attributes::new(console.0)?;
let mut startup = STARTUPINFOEXW::default();
startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
startup.lpAttributeList = attributes.raw();
let executable = wide(shell.as_os_str())?;
let directory = wide(root.as_os_str())?;
let mut command = vec![b'"' as u16];
command.extend_from_slice(&executable[..executable.len() - 1]);
command.extend_from_slice(&[b'"' as u16, 0]);
let mut info = PROCESS_INFORMATION::default();
if unsafe {
CreateProcessW(
executable.as_ptr(),
command.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED,
std::ptr::null(),
directory.as_ptr(),
&startup.StartupInfo,
&mut info,
)
} == 0
{
return Err(error(std::io::Error::last_os_error()));
}
let process = Handle::new(info.hProcess)?;
let thread = Handle::new(info.hThread)?;
let job = match Self::job(process.raw()) {
Ok(job) => job,
Err(error) => {
unsafe {
TerminateProcess(process.raw(), 1);
WaitForSingleObject(process.raw(), 5000);
}
return Err(error);
}
};
if unsafe { ResumeThread(thread.raw()) } == u32::MAX {
unsafe {
TerminateProcess(process.raw(), 1);
}
return Err(error(std::io::Error::last_os_error()));
}
let sink = Arc::new(Mutex::new(Buffer {
data: Vec::new(),
truncated: false,
}));
let stop = Arc::new(AtomicBool::new(false));
let thread_stop = stop.clone();
let thread_sink = sink.clone();
let reader = std::thread::spawn(move || {
let mut bytes = [0; 8192];
while !thread_stop.load(Ordering::Relaxed) {
let mut available = 0;
if unsafe {
PeekNamedPipe(
output.as_raw_handle(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
&mut available,
std::ptr::null_mut(),
)
} == 0
{
break;
}
if available == 0 {
std::thread::sleep(Duration::from_millis(20));
continue;
}
let amount = (available as usize).min(bytes.len());
let Ok(amount) = output.read(&mut bytes[..amount]) else {
break;
};
if amount == 0 {
break;
}
let Ok(mut buffer) = thread_sink.lock() else {
break;
};
let room = MAX_OUTPUT.saturating_sub(buffer.data.len());
buffer.data.extend_from_slice(&bytes[..amount.min(room)]);
if amount > room {
buffer.truncated = true;
}
}
});
let input = super::terminal_input::InputPump::new(input)?;
Ok(Self {
input: Some(input),
process,
job: Some(job),
console: Some(console),
output: sink,
stop,
reader: Some(reader),
})
}
fn job(process: HANDLE) -> Result<Handle, String> {
let job = Handle::new(unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) })?;
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if unsafe {
SetInformationJobObject(
job.raw(),
JobObjectExtendedLimitInformation,
(&limits as *const JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of_val(&limits) as u32,
)
} == 0
|| unsafe { AssignProcessToJobObject(job.raw(), process) } == 0
{
return Err(error(std::io::Error::last_os_error()));
}
Ok(job)
}
pub fn read(&mut self) -> Result<Value, String> {
let alive = unsafe { WaitForSingleObject(self.process.raw(), 0) } == 258;
if !alive {
drop(self.job.take());
drop(self.console.take());
self.input.take();
}
let mut buffer = self.output.lock().map_err(error)?;
let count = buffer.data.len().min(MAX_TERMINAL_READ);
let data: Vec<u8> = buffer.data.drain(..count).collect();
let truncated = std::mem::take(&mut buffer.truncated);
Ok(
json!({"output":String::from_utf8_lossy(&data),"bytes":data,"alive":alive,"drained":!alive && buffer.data.is_empty() && self.reader.as_ref().is_none_or(|reader|reader.is_finished()),"truncated":truncated}),
)
}
pub fn write(&mut self, bytes: &[u8]) -> Result<(), String> {
self.input
.as_ref()
.ok_or("Terminal input closed")?
.write(bytes)
}
pub fn resize(&mut self, columns: u16, rows: u16) -> Result<(), String> {
let x = i16::try_from(columns).map_err(|_| "ConPTY columns exceed 32767")?;
let y = i16::try_from(rows).map_err(|_| "ConPTY rows exceed 32767")?;
let console = self.console.as_ref().ok_or("Terminal process has ended")?;
let status = unsafe { ResizePseudoConsole(console.0, COORD { X: x, Y: y }) };
if status < 0 {
Err(format!("ResizePseudoConsole failed: {status:#x}"))
} else {
Ok(())
}
}
}
impl Drop for Session {
fn drop(&mut self) {
drop(self.job.take());
unsafe {
WaitForSingleObject(self.process.raw(), 5000);
}
drop(self.console.take());
self.input.take();
self.stop.store(true, Ordering::Relaxed);
if let Some(reader) = self.reader.take() {
let _ = reader.join();
}
}
}