use std::sync::Arc;
use tokio::sync::{Notify, mpsc};
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::{ReadFile, WriteFile};
use windows_sys::Win32::System::Console::{
COORD, ClosePseudoConsole, CreatePseudoConsole, HPCON, ResizePseudoConsole,
};
use windows_sys::Win32::System::Pipes::CreatePipe;
use windows_sys::Win32::System::Threading::{
CreateProcessW, EXTENDED_STARTUPINFO_PRESENT, GetExitCodeProcess,
InitializeProcThreadAttributeList, LPPROC_THREAD_ATTRIBUTE_LIST,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, PROCESS_INFORMATION, STARTUPINFOEXW,
UpdateProcThreadAttribute, WaitForSingleObject,
};
use crate::{AppState, PTY_CHANNEL_CAPACITY, PtyInput};
#[derive(Clone, Copy)]
pub struct PtyWriteTarget(pub HANDLE);
unsafe impl Send for PtyWriteTarget {}
unsafe impl Sync for PtyWriteTarget {}
pub struct PtyHandle {
pub(crate) conpty: HPCON,
pub(crate) process: HANDLE,
pub(crate) input: HANDLE,
pub(crate) output: HANDLE,
}
unsafe impl Send for PtyHandle {}
unsafe impl Sync for PtyHandle {}
pub fn pty_write_all(handle: PtyWriteTarget, mut data: &[u8]) {
while !data.is_empty() {
let mut written: u32 = 0;
let ok = unsafe {
WriteFile(
handle.0,
data.as_ptr(),
data.len() as u32,
&mut written,
std::ptr::null_mut(),
)
};
if ok == 0 || written == 0 {
break;
}
data = &data[written as usize..];
}
}
pub fn pty_lflag(_handle: &PtyHandle) -> (bool, bool) {
(false, false)
}
pub fn pty_cwd(_handle: &PtyHandle) -> Option<String> {
None
}
pub fn resize_pty_os(handle: &PtyHandle, rows: u16, cols: u16) {
let size = COORD {
X: cols as i16,
Y: rows as i16,
};
unsafe {
ResizePseudoConsole(handle.conpty, size);
}
}
pub fn kill_pty(handle: &PtyHandle, signal: i32) {
match signal {
2 => pty_write_all(PtyWriteTarget(handle.input), b"\x03"),
_ => unsafe {
windows_sys::Win32::System::Threading::TerminateProcess(handle.process, 1);
},
}
}
pub fn close_pty(handle: &PtyHandle) {
unsafe {
ClosePseudoConsole(handle.conpty);
CloseHandle(handle.input);
CloseHandle(handle.output);
CloseHandle(handle.process);
}
}
pub fn collect_exit_status(handle: &PtyHandle) -> i32 {
const STILL_ACTIVE: u32 = 259;
unsafe {
WaitForSingleObject(handle.process, 1000);
let mut exit_code: u32 = 0;
if GetExitCodeProcess(handle.process, &mut exit_code) != 0 && exit_code != STILL_ACTIVE {
exit_code as i32
} else {
blit_remote::EXIT_STATUS_UNKNOWN
}
}
}
pub fn reap_zombies() {}
pub fn respond_to_queries(handle: &PtyHandle, data: &[u8], size: (u16, u16), cursor: (u16, u16)) {
for resp in crate::parse_terminal_queries(data, size, cursor) {
pty_write_all(PtyWriteTarget(handle.input), resp.as_bytes());
}
}
pub(crate) struct SendHandle(pub(crate) HANDLE);
unsafe impl Send for SendHandle {}
pub(crate) fn pty_reader(handle: SendHandle, tx: mpsc::Sender<PtyInput>, notify: Arc<Notify>) {
let handle = handle.0;
let mut buf = vec![0u8; 64 * 1024];
let mut sync_scan_tail = Vec::new();
loop {
let mut bytes_read: u32 = 0;
let ok = unsafe {
ReadFile(
handle,
buf.as_mut_ptr(),
buf.len() as u32,
&mut bytes_read,
std::ptr::null_mut(),
)
};
if ok == 0 || bytes_read == 0 {
let _ = tx.blocking_send(PtyInput::Eof);
notify.notify_one();
return;
}
let data = buf[..bytes_read as usize].to_vec();
let mut remaining = data;
loop {
if remaining.is_empty() {
break;
}
if let Some(boundary) = crate::find_sync_output_end(&sync_scan_tail, &remaining) {
let before = remaining[..boundary].to_vec();
let after = remaining[boundary..].to_vec();
crate::update_sync_scan_tail(&mut sync_scan_tail, &before);
if tx.blocking_send(PtyInput::SyncBoundary { before }).is_err() {
return;
}
notify.notify_one();
remaining = after;
} else {
crate::update_sync_scan_tail(&mut sync_scan_tail, &remaining);
if tx.blocking_send(PtyInput::Data(remaining)).is_err() {
return;
}
notify.notify_one();
break;
}
}
}
}
fn create_pipe_pair() -> Option<(HANDLE, HANDLE)> {
let mut read_handle: HANDLE = INVALID_HANDLE_VALUE;
let mut write_handle: HANDLE = INVALID_HANDLE_VALUE;
let ok = unsafe { CreatePipe(&mut read_handle, &mut write_handle, std::ptr::null(), 0) };
if ok == 0 {
return None;
}
Some((read_handle, write_handle))
}
fn to_wide(s: &str) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
std::ffi::OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn build_command_line(shell: &str, shell_flags: &str, command: Option<&str>) -> Vec<u16> {
let shell_lower = shell.to_ascii_lowercase();
let is_cmd = shell_lower.ends_with("cmd.exe") || shell_lower.ends_with("cmd");
let cmd = if let Some(command) = command {
if is_cmd {
format!("{shell} /c {command}")
} else if shell_flags.is_empty() {
format!("{shell} -c {command}")
} else {
format!("{shell} -{shell_flags}c {command}")
}
} else if shell_flags.is_empty() {
shell.to_string()
} else {
format!("{shell} -{shell_flags}")
};
to_wide(&cmd)
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_pty(
shell: &str,
shell_flags: &str,
rows: u16,
cols: u16,
id: u16,
tag: &str,
command: Option<&str>,
_argv: Option<&[&str]>,
dir: Option<&str>,
scrollback: usize,
state: AppState,
_wayland_display: Option<&str>,
_pulse_server: Option<&str>,
_pipewire_remote: Option<&str>,
) -> Option<crate::Pty> {
let (input_read, input_write) = create_pipe_pair()?;
let (output_read, output_write) = create_pipe_pair()?;
let size = COORD {
X: cols as i16,
Y: rows as i16,
};
let mut conpty: HPCON = 0;
let hr = unsafe { CreatePseudoConsole(size, input_read, output_write, 0, &mut conpty) };
if hr != 0 {
unsafe {
CloseHandle(input_read);
CloseHandle(input_write);
CloseHandle(output_read);
CloseHandle(output_write);
}
eprintln!("CreatePseudoConsole failed for pty {id}: HRESULT 0x{hr:08x}");
return None;
}
let mut attr_list_size: usize = 0;
unsafe {
InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut attr_list_size);
}
let mut attr_list_buf = vec![0u8; attr_list_size];
let attr_list = attr_list_buf.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST;
unsafe {
if InitializeProcThreadAttributeList(attr_list, 1, 0, &mut attr_list_size) == 0 {
ClosePseudoConsole(conpty);
CloseHandle(input_read);
CloseHandle(input_write);
CloseHandle(output_read);
CloseHandle(output_write);
return None;
}
if UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE as usize,
conpty as *mut _,
std::mem::size_of::<HPCON>(),
std::ptr::null_mut(),
std::ptr::null(),
) == 0
{
ClosePseudoConsole(conpty);
CloseHandle(input_read);
CloseHandle(input_write);
CloseHandle(output_read);
CloseHandle(output_write);
return None;
}
}
let mut cmd_line = build_command_line(shell, shell_flags, command);
let dir_wide = dir.map(|d| to_wide(d));
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
si.lpAttributeList = attr_list;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmd_line.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
EXTENDED_STARTUPINFO_PRESENT,
std::ptr::null(),
dir_wide
.as_ref()
.map(|d| d.as_ptr())
.unwrap_or(std::ptr::null()),
&si.StartupInfo,
&mut pi,
)
};
unsafe {
CloseHandle(input_read);
CloseHandle(output_write);
}
if ok == 0 {
let err = unsafe { GetLastError() };
eprintln!("CreateProcessW failed for pty {id}: error {err}");
unsafe {
ClosePseudoConsole(conpty);
CloseHandle(input_write);
CloseHandle(output_read);
}
return None;
}
unsafe {
CloseHandle(pi.hThread);
}
let handle = PtyHandle {
conpty,
process: pi.hProcess,
input: input_write,
output: output_read,
};
state
.pty_fds
.write()
.unwrap()
.insert(id, PtyWriteTarget(handle.input));
let (byte_tx, byte_rx) = mpsc::channel(PTY_CHANNEL_CAPACITY);
let reader_output = SendHandle(handle.output);
let notify = state.delivery_notify.clone();
let reader_handle = std::thread::Builder::new()
.name(format!("pty-reader-{id}"))
.spawn(move || pty_reader(reader_output, byte_tx, notify))
.expect("failed to spawn pty-reader thread");
let lflag_cache = pty_lflag(&handle);
Some(crate::Pty {
handle,
driver: Box::new(blit_alacritty::TerminalDriver::new(rows, cols, scrollback)),
tag: tag.to_owned(),
dirty: true,
ready_frames: std::collections::VecDeque::new(),
byte_rx,
reader_handle,
lflag_cache,
lflag_last: std::time::Instant::now(),
last_title_send: std::time::Instant::now(),
title_pending: false,
exited: false,
exit_status: blit_remote::EXIT_STATUS_UNKNOWN,
command: command.map(|s| s.to_owned()),
})
}
pub fn respawn_child(
shell: &str,
shell_flags: &str,
rows: u16,
cols: u16,
pty_id: u16,
command: Option<&str>,
state: AppState,
_wayland_display: Option<&str>,
_pulse_server: Option<&str>,
_pipewire_remote: Option<&str>,
) -> Option<(
PtyHandle,
std::thread::JoinHandle<()>,
mpsc::Receiver<PtyInput>,
)> {
let (input_read, input_write) = create_pipe_pair()?;
let (output_read, output_write) = create_pipe_pair()?;
let size = COORD {
X: cols as i16,
Y: rows as i16,
};
let mut conpty: HPCON = 0;
let hr = unsafe { CreatePseudoConsole(size, input_read, output_write, 0, &mut conpty) };
if hr != 0 {
unsafe {
CloseHandle(input_read);
CloseHandle(input_write);
CloseHandle(output_read);
CloseHandle(output_write);
}
return None;
}
let mut attr_list_size: usize = 0;
unsafe {
InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut attr_list_size);
}
let mut attr_list_buf = vec![0u8; attr_list_size];
let attr_list = attr_list_buf.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST;
unsafe {
if InitializeProcThreadAttributeList(attr_list, 1, 0, &mut attr_list_size) == 0 {
ClosePseudoConsole(conpty);
CloseHandle(input_read);
CloseHandle(input_write);
CloseHandle(output_read);
CloseHandle(output_write);
return None;
}
if UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE as usize,
conpty as *mut _,
std::mem::size_of::<HPCON>(),
std::ptr::null_mut(),
std::ptr::null(),
) == 0
{
ClosePseudoConsole(conpty);
CloseHandle(input_read);
CloseHandle(input_write);
CloseHandle(output_read);
CloseHandle(output_write);
return None;
}
}
let mut cmd_line = build_command_line(shell, shell_flags, command);
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
si.lpAttributeList = attr_list;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmd_line.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
EXTENDED_STARTUPINFO_PRESENT,
std::ptr::null(),
std::ptr::null(),
&si.StartupInfo,
&mut pi,
)
};
unsafe {
CloseHandle(input_read);
CloseHandle(output_write);
}
if ok == 0 {
unsafe {
ClosePseudoConsole(conpty);
CloseHandle(input_write);
CloseHandle(output_read);
}
return None;
}
unsafe {
CloseHandle(pi.hThread);
}
let handle = PtyHandle {
conpty,
process: pi.hProcess,
input: input_write,
output: output_read,
};
state
.pty_fds
.write()
.unwrap()
.insert(pty_id, PtyWriteTarget(handle.input));
let (byte_tx, byte_rx) = mpsc::channel(PTY_CHANNEL_CAPACITY);
let reader_output = SendHandle(handle.output);
let notify = state.delivery_notify.clone();
let reader_handle = std::thread::Builder::new()
.name(format!("pty-reader-{pty_id}"))
.spawn(move || pty_reader(reader_output, byte_tx, notify))
.expect("failed to spawn pty-reader thread");
Some((handle, reader_handle, byte_rx))
}