diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-05 19:34:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-05 19:34:24 +0300 |
| commit | 2863b715132fe29e53089e06f105943d1df0b3b4 (patch) | |
| tree | 6b4af2f6fdec6de2888fd2bff3c0829d6719b727 /src/platform/windows.rs | |
| parent | eb93ec8ce62b05de776f751f4d2f52ff60f93f16 (diff) | |
feat(windows): support remote attach to unix hosts (#2329)
* feat(windows): support remote attach to unix hosts
* fix(remote): preserve unix bridge behavior
Diffstat (limited to 'src/platform/windows.rs')
| -rw-r--r-- | src/platform/windows.rs | 201 |
1 files changed, 196 insertions, 5 deletions
diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 55e0fd0b..0c9971fb 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,6 +12,8 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; +mod clipboard_image; + use windows_sys::{ Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation}, Win32::{ @@ -20,9 +22,14 @@ use windows_sys::{ NTSTATUS, STATUS_SUCCESS, UNICODE_STRING, }, Globalization::{CompareStringOrdinal, CSTR_EQUAL, CSTR_GREATER_THAN, CSTR_LESS_THAN}, + Security::SECURITY_ATTRIBUTES, + Storage::FileSystem::CreateDirectoryW, System::{ Console::GetConsoleWindow, - DataExchange::{CloseClipboard, EmptyClipboard, OpenClipboard, SetClipboardData}, + DataExchange::{ + CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard, + RegisterClipboardFormatW, SetClipboardData, + }, Diagnostics::{ Debug::ReadProcessMemory, ToolHelp::{ @@ -35,10 +42,10 @@ use windows_sys::{ JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, }, Memory::{ - GlobalAlloc, GlobalLock, GlobalUnlock, VirtualQueryEx, GMEM_MOVEABLE, + GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, VirtualQueryEx, GMEM_MOVEABLE, MEMORY_BASIC_INFORMATION, }, - Ole::CF_UNICODETEXT, + Ole::{CF_DIB, CF_DIBV5, CF_UNICODETEXT}, Threading::{ GetCurrentProcess, GetExitCodeProcess, GetProcessTimes, OpenProcess, QueryFullProcessImageNameW, TerminateProcess, CREATE_NO_WINDOW, DETACHED_PROCESS, @@ -83,6 +90,111 @@ static PROCESS_RUNTIME_MARKER_CACHE: LazyLock<Mutex<HashMap<u32, CachedProcessRu static GIT_BASH_PROCESS_CACHE: LazyLock<Mutex<HashMap<u32, CachedGitBashProcess>>> = LazyLock::new(|| Mutex::new(HashMap::new())); +pub(crate) fn remote_ssh_config_paths() -> super::RemoteSshConfigPaths { + super::RemoteSshConfigPaths { + user_config: std::env::var_os("USERPROFILE") + .map(PathBuf::from) + .map(|home| home.join(".ssh").join("config")), + system_config: std::env::var_os("PROGRAMDATA") + .map(PathBuf::from) + .map(|dir| dir.join("ssh").join("ssh_config")), + multiplexing: false, + } +} + +pub(crate) fn create_remote_ssh_config_dir(_control_socket_name: &str) -> std::io::Result<PathBuf> { + let base = remote_private_temp_base(); + std::fs::create_dir_all(&base)?; + for attempt in 0..100 { + let dir = base.join(format!("ssh-{}-{attempt}", std::process::id())); + match create_remote_private_dir(&dir) { + Ok(()) => return Ok(dir), + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(err) => return Err(err), + } + } + Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "failed to create private herdr ssh config directory", + )) +} + +pub(crate) fn create_remote_ssh_config_file( + path: &std::path::Path, +) -> std::io::Result<std::fs::File> { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) +} + +pub(crate) fn create_remote_private_dir(path: &std::path::Path) -> std::io::Result<()> { + use interprocess::os::windows::security_descriptor::{ + AsSecurityDescriptorExt as _, SecurityDescriptor, + }; + use widestring::U16CString; + + let sddl = U16CString::from_str("D:P(A;OICI;GA;;;SY)(A;OICI;GA;;;OW)") + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; + let security_descriptor = SecurityDescriptor::deserialize(&sddl)?; + let mut security_attributes = SECURITY_ATTRIBUTES { + nLength: u32::try_from(size_of::<SECURITY_ATTRIBUTES>()).unwrap_or(u32::MAX), + lpSecurityDescriptor: null_mut(), + bInheritHandle: 0, + }; + security_descriptor.write_to_security_attributes(&mut security_attributes); + let path = extended_length_path(path)?; + if unsafe { CreateDirectoryW(path.as_ptr(), &security_attributes) } != 0 { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + } +} + +fn extended_length_path(path: &std::path::Path) -> std::io::Result<Vec<u16>> { + use std::os::windows::ffi::OsStrExt as _; + + let path = std::path::absolute(path)?; + let wide = path.as_os_str().encode_wide().collect::<Vec<_>>(); + let mut extended = if wide.starts_with(&[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16]) + || wide.starts_with(&[b'\\' as u16, b'\\' as u16, b'.' as u16, b'\\' as u16]) + { + wide + } else if wide.starts_with(&[b'\\' as u16, b'\\' as u16]) { + "\\\\?\\UNC\\" + .encode_utf16() + .chain(wide.into_iter().skip(2)) + .collect() + } else { + "\\\\?\\".encode_utf16().chain(wide).collect() + }; + extended.push(0); + Ok(extended) +} + +pub(crate) fn remote_private_temp_base() -> PathBuf { + crate::config::state_dir().join("remote") +} + +pub(crate) fn remote_bridge_endpoint_path(_readable_name: &str, short_name: &str) -> PathBuf { + remote_private_temp_base().join(short_name) +} + +pub(crate) fn remote_reattach_program(program: &str) -> String { + let path = std::env::current_exe() + .ok() + .filter(|path| path.is_absolute()) + .unwrap_or_else(|| PathBuf::from(program)); + format!( + "& {}", + remote_reattach_argument(&path.display().to_string()) + ) +} + +pub(crate) fn remote_reattach_argument(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} + /// Encode native or targeted semantic Win32 input for a compatible ConPTY destination. pub(crate) fn encode_windows_conpty_fallback(key: &crate::input::TerminalKey) -> Option<Vec<u8>> { use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; @@ -1242,12 +1354,76 @@ pub fn open_url(url: &str) -> std::io::Result<()> { } } -// Windows does not wire clipboard-image bridging into semantic input yet. -#[cfg_attr(windows, allow(dead_code))] pub fn read_clipboard_image() -> Option<ClipboardImage> { + for attempt in 0..10 { + if unsafe { OpenClipboard(null_mut()) } != 0 { + let _clipboard = ClipboardGuard; + if let Some(bytes) = read_registered_png_clipboard() { + return Some(ClipboardImage { + bytes, + extension: "png", + }); + } + for format in [CF_DIBV5 as u32, CF_DIB as u32] { + if let Some(bytes) = + clipboard_global_bytes(format, clipboard_image::MAX_CLIPBOARD_ALLOCATION) + { + if let Some(bytes) = clipboard_image::dib_to_png(&bytes) { + return Some(ClipboardImage { + bytes, + extension: "png", + }); + } + } + } + return None; + } + if attempt < 9 { + std::thread::sleep(Duration::from_millis(5)); + } + } None } +fn read_registered_png_clipboard() -> Option<Vec<u8>> { + static PNG_FORMAT: LazyLock<u32> = LazyLock::new(|| { + let name = wide_null("PNG"); + unsafe { RegisterClipboardFormatW(name.as_ptr()) } + }); + if *PNG_FORMAT == 0 { + return None; + } + let bytes = clipboard_global_bytes( + *PNG_FORMAT, + crate::protocol::MAX_CLIPBOARD_IMAGE_PAYLOAD + 64 * 1024, + )?; + clipboard_image::validated_png(&bytes) +} + +fn clipboard_global_bytes(format: u32, max_bytes: usize) -> Option<Vec<u8>> { + let handle = unsafe { GetClipboardData(format) }; + if handle.is_null() { + return None; + } + let data = unsafe { GlobalLock(handle) }; + if data.is_null() { + return None; + } + let size = unsafe { GlobalSize(handle) }; + if size == 0 || size > max_bytes { + unsafe { + GlobalUnlock(handle); + } + return None; + } + let mut bytes = vec![0_u8; size]; + unsafe { + copy_nonoverlapping(data.cast::<u8>(), bytes.as_mut_ptr(), size); + GlobalUnlock(handle); + } + Some(bytes) +} + pub fn show_desktop_notification(title: &str, body: Option<&str>) -> std::io::Result<bool> { let title = title.to_owned(); let body = body.unwrap_or(&title).to_owned(); @@ -1788,6 +1964,21 @@ mod tests { }; #[test] + fn private_remote_directory_supports_long_paths() { + let base = std::env::temp_dir().join(format!( + "herdr-private-remote-dir-test-{}", + std::process::id() + )); + fs::create_dir_all(&base).expect("create test base"); + let private = base.join("x".repeat(240)); + + super::create_remote_private_dir(&private).expect("create private long-path directory"); + fs::write(private.join("probe"), b"ok").expect("write inherited private file"); + + fs::remove_dir_all(base).expect("remove test directory"); + } + + #[test] fn windows_conpty_native_encoder_uses_canonical_phase_and_repeat_count() { let key = crate::input::TerminalKey::new( crossterm::event::KeyCode::Esc, |
