From 9e9bc8a144667a6b7debfd4f0c7a31ff0e11493a Mon Sep 17 00:00:00 2001 From: Can Celik Date: Sun, 6 Sep 2026 02:27:06 +0300 Subject: feat: manage multiple ssh machines from one client (#3670) --- src/platform/client_state.rs | 32 ++++++++++++++++++++++++++++++++ src/platform/linux.rs | 2 +- src/platform/macos.rs | 2 +- src/platform/mod.rs | 3 +++ src/platform/unix_common.rs | 19 +++++++++++++++++++ src/platform/windows.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/platform/client_state.rs (limited to 'src/platform') diff --git a/src/platform/client_state.rs b/src/platform/client_state.rs new file mode 100644 index 00000000..d339bae8 --- /dev/null +++ b/src/platform/client_state.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +#[cfg(not(windows))] +pub(crate) fn create_private_state_file(path: &Path) -> std::io::Result { + super::create_remote_ssh_config_file(path) +} + +#[cfg(windows)] +pub(crate) fn create_private_state_file(path: &Path) -> std::io::Result { + super::windows::create_remote_ssh_config_file(path) +} + +#[cfg(not(windows))] +pub(crate) fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> { + std::fs::rename(source, destination) +} + +#[cfg(windows)] +pub(crate) fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> { + super::windows::replace_file(source, destination) +} + +#[cfg(not(windows))] +pub(crate) fn sync_parent_directory(path: &Path) -> std::io::Result<()> { + std::fs::File::open(path)?.sync_all() +} + +#[cfg(windows)] +pub(crate) fn sync_parent_directory(_path: &Path) -> std::io::Result<()> { + // replace_file uses MOVEFILE_WRITE_THROUGH on Windows. + Ok(()) +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8efe7357..6c09667a 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -17,7 +17,7 @@ pub(crate) use super::unix_common::{ create_remote_ssh_config_file, hostname, local_datetime, remote_bridge_endpoint_path, remote_private_temp_base, remote_reattach_argument, remote_reattach_program, remote_ssh_config_paths, set_default_plugin_pane_pwd, status_commands_supported, - StatusCommandGuard, + wait_client_stream_readable, StatusCommandGuard, }; const WSL_MARKER_ENV_VARS: &[&str] = &["WSL_DISTRO_NAME", "WSL_INTEROP"]; diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 948468ac..8840c7ed 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,7 +17,7 @@ pub(crate) use super::unix_common::{ create_remote_ssh_config_file, hostname, local_datetime, remote_bridge_endpoint_path, remote_private_temp_base, remote_reattach_argument, remote_reattach_program, remote_ssh_config_paths, set_default_plugin_pane_pwd, status_commands_supported, - StatusCommandGuard, + wait_client_stream_readable, StatusCommandGuard, }; const PROC_PGRP_ONLY: u32 = 2; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 4b49b9f7..86a431aa 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -236,6 +236,9 @@ mod unix_common; #[cfg(unix)] pub(crate) use unix_common::{begin_cli_output, end_cli_output}; +mod client_state; +pub(crate) use client_state::{create_private_state_file, replace_file, sync_parent_directory}; + #[cfg(not(unix))] pub(crate) fn begin_cli_output() {} diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs index a7926c22..dff67357 100644 --- a/src/platform/unix_common.rs +++ b/src/platform/unix_common.rs @@ -1,5 +1,24 @@ use std::path::{Path, PathBuf}; +pub(crate) fn wait_client_stream_readable(stream: &crate::ipc::LocalStream) -> std::io::Result<()> { + use std::os::fd::{AsFd as _, AsRawFd as _}; + let crate::ipc::LocalStream::UdSocket(stream) = stream; + let mut descriptor = libc::pollfd { + fd: stream.as_fd().as_raw_fd(), + events: libc::POLLIN, + revents: 0, + }; + // Bound cancellation latency without polling idle connections hundreds of times per second. + let result = unsafe { libc::poll(&mut descriptor, 1, 100) }; + if result < 0 { + let error = std::io::Error::last_os_error(); + if error.kind() != std::io::ErrorKind::Interrupted { + return Err(error); + } + } + Ok(()) +} + pub(super) fn read_terminal_grid_size() -> std::io::Result<(u16, u16)> { crossterm::terminal::window_size().map(|size| (size.columns, size.rows)) } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 1c5c4afb..ce0afedb 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -15,10 +15,52 @@ use std::{ mod clipboard_image; +pub(crate) fn wait_client_stream_readable( + _stream: &crate::ipc::LocalStream, +) -> std::io::Result<()> { + // Sync named pipes have no read timeout. The caller peeks before each read and checks its + // cancellation flag between polls, including when a frame arrives in several fragments. + std::thread::sleep(Duration::from_millis(2)); + Ok(()) +} + pub(super) fn read_terminal_grid_size() -> std::io::Result<(u16, u16)> { crossterm::terminal::size() } +pub(crate) fn replace_file( + source: &std::path::Path, + destination: &std::path::Path, +) -> std::io::Result<()> { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Storage::FileSystem::{ + MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, + }; + + let source = source + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let destination = destination + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let moved = unsafe { + MoveFileExW( + source.as_ptr(), + destination.as_ptr(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, + ) + }; + if moved == 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } +} + pub(crate) fn set_default_plugin_pane_pwd( _env: &mut Vec<(String, String)>, _cwd: &std::path::Path, -- cgit v1.2.3