diff --git a/Cargo.lock b/Cargo.lock index 8af19a7..4e2cfe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,12 +131,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.0" @@ -174,12 +168,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -272,7 +260,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "crossterm_winapi", "derive_more", "document-features", @@ -344,12 +332,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "either" version = "1.15.0" @@ -378,17 +360,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -796,7 +767,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -824,7 +795,6 @@ dependencies = [ "jsonpath-rust", "k8s-openapi", "kube", - "portable-pty", "prost", "prost-build", "protoc-bin-vendored", @@ -836,7 +806,7 @@ dependencies = [ "serde_json", "signal-hook", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-vsock", "tower", @@ -890,7 +860,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tower", @@ -912,15 +882,9 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror 2.0.18", + "thiserror", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1005,27 +969,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nix" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -1186,27 +1138,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix 0.28.0", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg", -] - [[package]] name = "pratt" version = "0.4.0" @@ -1377,7 +1308,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -1438,7 +1369,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1528,7 +1459,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -1617,17 +1548,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial2" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1401f562d358cdfdbdf8946e51a7871ede1db68bd0fd99bedc79e400241550" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1639,22 +1559,6 @@ dependencies = [ "digest", ] -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shlex" version = "1.3.0" @@ -1762,33 +1666,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1890,7 +1774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "base64", - "bitflags 2.11.0", + "bitflags", "bytes", "http", "http-body", @@ -2022,7 +1906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b82aeb12ad864eb8cd26a6c21175d0bdc66d398584ee6c93c76964c3bcfc78ff" dependencies = [ "libc", - "nix 0.31.2", + "nix", ] [[package]] @@ -2086,7 +1970,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -2285,15 +2169,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2352,7 +2227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap", "log", "serde", diff --git a/crates/kino/Cargo.toml b/crates/kino/Cargo.toml index 96cbe4d..6d7138d 100644 --- a/crates/kino/Cargo.toml +++ b/crates/kino/Cargo.toml @@ -35,11 +35,8 @@ tempfile = "3.26.0" tower = { version = "0.5.3", features = ["util"] } [target.'cfg(target_os = "linux")'.dependencies] +crossterm = "0.29.0" pty-process = "0.5.3" rustix = { version = "1.1.4", features = ["event", "pipe"] } -tokio-vsock = { version = "0.7.2", features = ["axum08"] } - -[target.'cfg(unix)'.dependencies] -crossterm = "0.29.0" -portable-pty = "0.9.0" signal-hook = "0.3.18" +tokio-vsock = { version = "0.7.2", features = ["axum08"] } diff --git a/crates/kino/src/recording.rs b/crates/kino/src/recording.rs index e589496..31249f7 100644 --- a/crates/kino/src/recording.rs +++ b/crates/kino/src/recording.rs @@ -1,27 +1,18 @@ pub(crate) use imp::{record_command, record_ssh}; -#[cfg(unix)] +#[cfg(target_os = "linux")] mod imp { use crate::config::RecordingConfig; use anyhow::{Context, Result, anyhow, bail}; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use crossterm::terminal; - #[cfg(not(target_os = "linux"))] - use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system}; - #[cfg(target_os = "linux")] use pty_process::Size as BlockingPtySize; - #[cfg(target_os = "linux")] use pty_process::blocking::{Command as PtyCommand, Pty as BlockingPty, open as open_pty}; - #[cfg(target_os = "linux")] use rustix::event::{PollFd, PollFlags, Timespec, poll}; - #[cfg(target_os = "linux")] use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; - #[cfg(target_os = "linux")] use rustix::io::{Errno as RustixErrno, dup, read as fd_read, write as fd_write}; - #[cfg(target_os = "linux")] use rustix::pipe::{PipeFlags, pipe_with}; - #[cfg(target_os = "linux")] use rustix::process::{Pid, Signal, kill_process_group}; use serde::Serialize; use signal_hook::consts::signal::SIGWINCH; @@ -30,18 +21,14 @@ mod imp { use std::fs::{self, File, OpenOptions}; use std::io::ErrorKind; use std::io::{self, IsTerminal, Read, Write}; - #[cfg(target_os = "linux")] use std::os::fd::{AsFd, OwnedFd}; - #[cfg(target_os = "linux")] use std::os::unix::process::ExitStatusExt; use std::path::{Path, PathBuf}; - #[cfg(target_os = "linux")] use std::process::ExitStatus; use std::process::{ChildStdin, Command, Stdio}; use std::sync::mpsc; use std::sync::{Arc, Mutex}; use std::thread; - #[cfg(target_os = "linux")] use std::time::Instant; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -50,9 +37,7 @@ mod imp { const RECORDING_SYNC_INTERVAL_MS: u64 = 250; const RAW_RECORDING_VERSION: u8 = 1; const RAW_RECORDING_FORMAT: &str = "kino.raw-event-log"; - #[cfg(target_os = "linux")] const INTERACTIVE_POLL_INTERVAL: Duration = Duration::from_millis(50); - #[cfg(target_os = "linux")] const INTERACTIVE_DRAIN_QUIET_PERIOD: Duration = Duration::from_millis(500); #[derive(Debug, Serialize)] @@ -275,19 +260,11 @@ mod imp { Ok(exit_code) } - #[cfg(target_os = "linux")] pub(crate) fn record_ssh(config: &RecordingConfig, command: Option<&str>) -> Result { ensure_interactive_tty()?; record_ssh_linux(config, command) } - #[cfg(not(target_os = "linux"))] - pub(crate) fn record_ssh(config: &RecordingConfig, command: Option<&str>) -> Result { - ensure_interactive_tty()?; - record_ssh_portable(config, command) - } - - #[cfg(target_os = "linux")] fn record_ssh_linux(config: &RecordingConfig, command: Option<&str>) -> Result { let (width, height) = tty_dimensions(); let (shared_writer, recording_path, _raw_mode) = @@ -297,8 +274,7 @@ mod imp { } let mut session = LinuxInteractiveSession::start(&config.real_shell, width, height, command)?; - let loop_result = - run_linux_interactive_loop(&mut session, Arc::clone(&shared_writer), &recording_path); + let loop_result = run_linux_interactive_loop(&mut session, &shared_writer, &recording_path); if loop_result.is_err() { best_effort_terminate_process_group(session.child_pid); } @@ -325,71 +301,6 @@ mod imp { Ok(exit_code) } - #[cfg(not(target_os = "linux"))] - fn record_ssh_portable(config: &RecordingConfig, command: Option<&str>) -> Result { - let (width, height) = tty_dimensions(); - let (shared_writer, recording_path, _raw_mode) = - prepare_interactive_writer(config, width, height, command)?; - if let Some(command) = command { - write_command_input_event(&shared_writer, &recording_path, command)?; - } - let input_error = Arc::new(Mutex::new(None::)); - let resize_error = Arc::new(Mutex::new(None::)); - let (child, pty_reader, pty_writer, pty_master, slave_keepalive) = - start_login_shell(&config.real_shell, width, height, command)?; - let mut child_killer = child.clone_killer(); - - spawn_input_forwarder( - pty_writer, - Arc::clone(&shared_writer), - Arc::clone(&input_error), - ); - let resize_handle = spawn_resize_forwarder( - pty_master, - Arc::clone(&shared_writer), - Arc::clone(&resize_error), - )?; - let output_handle = spawn_shell_output_forwarder(pty_reader, Arc::clone(&shared_writer)); - let wait_handle = spawn_shell_waiter(child, slave_keepalive); - - let output_result = join_shell_output_forwarder(output_handle); - if output_result.is_err() { - let _ = child_killer.kill(); - } - - let exit_code = join_shell_waiter(wait_handle)?; - resize_handle.close(); - - if let Some(message) = take_thread_error(&input_error) { - return Err(anyhow!(message)); - } - if let Some(message) = take_thread_error(&resize_error) { - return Err(anyhow!(message)); - } - - output_result?; - - { - let mut writer = shared_writer - .lock() - .map_err(|_| anyhow!("cast writer lock poisoned"))?; - writer.write_exit(unix_ms(), exit_code).with_context(|| { - format!( - "failed to write shell exit event to {}", - recording_path.display() - ) - })?; - writer.finish().with_context(|| { - format!( - "failed to flush recording file {}", - recording_path.display() - ) - })?; - } - - Ok(exit_code) - } - fn ensure_interactive_tty() -> Result<()> { let stdin = io::stdin(); let stdout = io::stdout(); @@ -439,13 +350,11 @@ mod imp { Ok((Arc::new(Mutex::new(writer)), recording_path, raw_mode)) } - #[cfg(target_os = "linux")] struct PendingInputBuffer { bytes: Vec, offset: usize, } - #[cfg(target_os = "linux")] impl PendingInputBuffer { fn new() -> Self { Self { @@ -479,7 +388,6 @@ mod imp { } } - #[cfg(target_os = "linux")] struct LinuxInteractiveSession { pty: BlockingPty, child_pid: Option, @@ -492,7 +400,6 @@ mod imp { resize_thread: thread::JoinHandle<()>, } - #[cfg(target_os = "linux")] impl LinuxInteractiveSession { fn start( real_shell: &Path, @@ -584,7 +491,6 @@ mod imp { } } - #[cfg(target_os = "linux")] fn run_linux_interactive_loop( session: &mut LinuxInteractiveSession, writer: &Arc>, @@ -627,12 +533,16 @@ mod imp { let timeout = Timespec::try_from(INTERACTIVE_POLL_INTERVAL) .context("failed to build PTY poll timeout")?; poll(&mut poll_fds, Some(&timeout)).context("failed polling PTY session fds")?; + let exit_ready = poll_fds[2].revents().contains(PollFlags::IN); + let resize_ready = poll_fds[3].revents().contains(PollFlags::IN); + let pty_events = poll_fds[0].revents(); + let stdin_ready = !stdin_closed && poll_fds[1].revents().contains(PollFlags::IN); - if poll_fds[2].revents().contains(PollFlags::IN) { + if exit_ready { handle_exit_notification(session, &mut exit_code, &mut exit_observed_at)?; } - if poll_fds[3].revents().contains(PollFlags::IN) { + if resize_ready { handle_resize_notification( session, writer, @@ -642,7 +552,6 @@ mod imp { )?; } - let pty_events = poll_fds[0].revents(); if pty_events.intersects(PollFlags::IN | PollFlags::HUP | PollFlags::ERR) { drain_pty_output( session, @@ -655,9 +564,8 @@ mod imp { )?; } - if !stdin_closed && poll_fds[1].revents().contains(PollFlags::IN) { + if stdin_ready { read_stdin_chunk( - &stdin, writer, recording_path, &mut stdin_buffer, @@ -684,7 +592,6 @@ mod imp { } } - #[cfg(target_os = "linux")] fn handle_exit_notification( session: &LinuxInteractiveSession, exit_code: &mut Option, @@ -698,7 +605,6 @@ mod imp { Ok(()) } - #[cfg(target_os = "linux")] fn handle_resize_notification( session: &mut LinuxInteractiveSession, writer: &Arc>, @@ -724,7 +630,6 @@ mod imp { Ok(()) } - #[cfg(target_os = "linux")] fn drain_pty_output( session: &LinuxInteractiveSession, writer: &Arc>, @@ -736,7 +641,7 @@ mod imp { ) -> Result<()> { let mut stdout = io::stdout(); loop { - match fd_read(&session.pty, pty_buffer) { + match fd_read(&session.pty, &mut *pty_buffer) { Ok(0) => { *pty_hup_seen = true; break; @@ -771,9 +676,7 @@ mod imp { Ok(()) } - #[cfg(target_os = "linux")] fn read_stdin_chunk( - stdin: &io::Stdin, writer: &Arc>, recording_path: &Path, stdin_buffer: &mut [u8; 4096], @@ -782,7 +685,8 @@ mod imp { exit_code: Option, pty_hup_seen: bool, ) -> Result<()> { - match fd_read(stdin, stdin_buffer) { + let stdin = io::stdin(); + match fd_read(stdin, &mut *stdin_buffer) { Ok(0) => *stdin_closed = true, Ok(read_count) => { let chunk = &stdin_buffer[..read_count]; @@ -802,7 +706,6 @@ mod imp { Ok(()) } - #[cfg(target_os = "linux")] fn should_finish_interactive_loop( exit_code: Option, pending_input_empty: bool, @@ -820,13 +723,11 @@ mod imp { && (pty_hup_seen || now.duration_since(quiet_start) >= INTERACTIVE_DRAIN_QUIET_PERIOD) } - #[cfg(target_os = "linux")] fn set_nonblocking(fd: &impl AsFd) -> Result<()> { let flags = fcntl_getfl(fd).context("failed to read PTY flags")?; fcntl_setfl(fd, flags | OFlags::NONBLOCK).context("failed to set PTY nonblocking mode") } - #[cfg(target_os = "linux")] fn flush_pending_input( pty: &BlockingPty, pending_input: &mut PendingInputBuffer, @@ -836,7 +737,7 @@ mod imp { match fd_write(pty, pending_input.remaining()) { Ok(0) => break, Ok(written) => pending_input.advance(written), - Err(error) if error.kind() == ErrorKind::Interrupted => continue, + Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) if error.kind() == ErrorKind::WouldBlock => break, Err(error) if is_expected_linux_pty_shutdown_error(error) || exit_known => { pending_input.advance(pending_input.remaining().len()); @@ -849,50 +750,43 @@ mod imp { Ok(()) } - #[cfg(target_os = "linux")] fn drain_notify_pipe(fd: &OwnedFd) -> Result<()> { let mut buffer = [0_u8; 64]; loop { match fd_read(fd, &mut buffer) { Ok(0) => return Ok(()), Ok(_) => {} - Err(error) if error.kind() == ErrorKind::Interrupted => continue, + Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(()), Err(error) => return Err(error).context("failed to drain notify pipe"), } } } - #[cfg(target_os = "linux")] fn notify_pipe(fd: &OwnedFd) { let _ = fd_write(fd, &[1]); } - #[cfg(target_os = "linux")] fn take_shared_result(slot: &Arc>>) -> Option { slot.lock().ok().and_then(|mut guard| guard.take()) } - #[cfg(target_os = "linux")] fn take_shared_value(slot: &Arc>>) -> Option { slot.lock().ok().and_then(|mut guard| guard.take()) } - #[cfg(target_os = "linux")] fn normalize_exit_status(status: ExitStatus) -> i32 { status .code() .unwrap_or_else(|| 128 + status.signal().unwrap_or(1)) } - #[cfg(target_os = "linux")] fn best_effort_terminate_process_group(pid: Option) { if let Some(pid) = pid { let _ = kill_process_group(pid, Signal::TERM); } } - #[cfg(target_os = "linux")] fn is_expected_linux_pty_shutdown_error(error: RustixErrno) -> bool { matches!( error, @@ -900,15 +794,6 @@ mod imp { ) } - #[cfg(not(target_os = "linux"))] - type PtyChild = Box; - #[cfg(not(target_os = "linux"))] - type PtyReader = Box; - #[cfg(not(target_os = "linux"))] - type PtyMaster = Box; - #[cfg(not(target_os = "linux"))] - type PtyWriter = Box; - #[derive(Debug, Clone, Copy)] enum CommandOutputStream { Stdout, @@ -1074,95 +959,6 @@ mod imp { .unwrap_or(1)) } - #[cfg(not(target_os = "linux"))] - fn start_login_shell( - real_shell: &Path, - width: u16, - height: u16, - command: Option<&str>, - ) -> Result<(PtyChild, PtyReader, PtyWriter, PtyMaster, Option)> { - let pty_system = native_pty_system(); - let pair = pty_system - .openpty(PtySize { - rows: height, - cols: width, - pixel_width: 0, - pixel_height: 0, - }) - .context("failed to allocate PTY")?; - - let mut builder = CommandBuilder::new(real_shell.to_string_lossy().into_owned()); - if let Some(command) = command { - builder.arg("-c"); - builder.arg(command); - } else { - builder.arg("-l"); - } - - let child = pair - .slave - .spawn_command(builder) - .with_context(|| format!("failed to launch login shell {}", real_shell.display()))?; - let pty_reader = pair - .master - .try_clone_reader() - .context("failed to clone PTY reader")?; - let pty_writer = pair - .master - .take_writer() - .context("failed to take PTY writer")?; - let slave_keepalive = open_slave_keepalive(pair.master.as_ref())?; - - Ok((child, pty_reader, pty_writer, pair.master, slave_keepalive)) - } - - #[cfg(not(target_os = "linux"))] - fn spawn_input_forwarder( - mut pty_writer: PtyWriter, - writer: Arc>, - error_slot: Arc>>, - ) { - let _input_thread = thread::spawn(move || { - let mut input = io::stdin(); - let mut buffer = [0_u8; 4096]; - - loop { - match input.read(&mut buffer) { - Ok(0) => break, - Ok(read_count) => { - if let Err(error) = pty_writer - .write_all(&buffer[..read_count]) - .and_then(|()| pty_writer.flush()) - { - if is_expected_pty_shutdown_error(&error) { - break; - } - store_thread_error( - &error_slot, - format!("failed to forward input to shell: {error}"), - ); - break; - } - if let Err(error) = - write_cast_chunk(&writer, unix_ms(), "i", &buffer[..read_count]) - { - store_thread_error( - &error_slot, - format!("failed to write input event: {error}"), - ); - break; - } - } - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => { - store_thread_error(&error_slot, format!("failed to read stdin: {error}")); - break; - } - } - } - }); - } - fn spawn_command_input_forwarder( mut child_stdin: ChildStdin, writer: Arc>, @@ -1212,44 +1008,6 @@ mod imp { }); } - #[cfg(not(target_os = "linux"))] - fn spawn_resize_forwarder( - pty_master: PtyMaster, - writer: Arc>, - error_slot: Arc>>, - ) -> Result { - let mut signals = Signals::new([SIGWINCH]).context("failed to subscribe to SIGWINCH")?; - let handle = signals.handle(); - - let _resize_thread = thread::spawn(move || { - for _ in signals.forever() { - let (width, height) = tty_dimensions(); - let size = PtySize { - rows: height, - cols: width, - pixel_width: 0, - pixel_height: 0, - }; - if let Err(error) = pty_master.resize(size) { - if is_expected_pty_shutdown_anyhow(&error) { - break; - } - store_thread_error(&error_slot, format!("failed to resize PTY: {error}")); - break; - } - if let Err(error) = write_resize_event(&writer, unix_ms(), width, height) { - store_thread_error( - &error_slot, - format!("failed to write resize event: {error}"), - ); - break; - } - } - }); - - Ok(handle) - } - fn spawn_command_output_forwarder( mut reader: R, stream: CommandOutputStream, @@ -1345,93 +1103,6 @@ mod imp { } } - #[cfg(not(target_os = "linux"))] - fn open_slave_keepalive(master: &dyn MasterPty) -> Result> { - let Some(tty_name) = master.tty_name() else { - return Ok(None); - }; - - let file = OpenOptions::new() - .read(true) - .write(true) - .open(&tty_name) - .with_context(|| { - format!( - "failed to open PTY slave keepalive handle {}", - tty_name.display() - ) - })?; - - Ok(Some(file)) - } - - #[cfg(not(target_os = "linux"))] - fn spawn_shell_output_forwarder( - mut pty_reader: PtyReader, - writer: Arc>, - ) -> thread::JoinHandle> { - thread::spawn(move || { - let mut stdout = io::stdout(); - let mut buffer = [0_u8; 4096]; - - loop { - match pty_reader.read(&mut buffer) { - Ok(0) => break, - Ok(read_count) => { - stdout - .write_all(&buffer[..read_count]) - .context("failed to forward shell output to stdout")?; - stdout.flush().context("failed to flush stdout")?; - write_cast_chunk(&writer, unix_ms(), "o", &buffer[..read_count]) - .context("failed to write output event")?; - } - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) if error.kind() == ErrorKind::WouldBlock => { - thread::sleep(Duration::from_millis(5)); - } - Err(error) if is_expected_pty_shutdown_error(&error) => break, - Err(error) => return Err(error).context("failed to read PTY output"), - } - } - - Ok(()) - }) - } - - #[cfg(not(target_os = "linux"))] - fn join_shell_output_forwarder(handle: thread::JoinHandle>) -> Result<()> { - handle - .join() - .map_err(|_| anyhow!("shell output forwarder thread panicked"))? - } - - #[cfg(not(target_os = "linux"))] - fn spawn_shell_waiter( - mut child: PtyChild, - slave_keepalive: Option, - ) -> thread::JoinHandle> { - thread::spawn(move || { - let status = child.wait().context("failed waiting for login shell")?; - drop(slave_keepalive); - Ok(i32::try_from(status.exit_code()).unwrap_or(1)) - }) - } - - #[cfg(not(target_os = "linux"))] - fn join_shell_waiter(handle: thread::JoinHandle>) -> Result { - handle - .join() - .map_err(|_| anyhow!("shell waiter thread panicked"))? - } - - #[cfg(not(target_os = "linux"))] - fn is_expected_pty_shutdown_error(error: &io::Error) -> bool { - matches!( - error.kind(), - ErrorKind::BrokenPipe | ErrorKind::ConnectionReset | ErrorKind::UnexpectedEof - ) || matches!(error.raw_os_error(), Some(5 | 32)) - } - fn is_expected_command_pipe_shutdown_error(error: &io::Error) -> bool { matches!( error.kind(), @@ -1439,14 +1110,6 @@ mod imp { ) || matches!(error.raw_os_error(), Some(32 | 104)) } - #[cfg(not(target_os = "linux"))] - fn is_expected_pty_shutdown_anyhow(error: &anyhow::Error) -> bool { - error - .chain() - .find_map(|cause| cause.downcast_ref::()) - .is_some_and(is_expected_pty_shutdown_error) - } - fn write_resize_event( writer: &Arc>, ts_unix_ms: u64, @@ -1699,16 +1362,16 @@ mod imp { } } -#[cfg(not(unix))] +#[cfg(not(target_os = "linux"))] mod imp { use crate::config::RecordingConfig; use anyhow::{Result, bail}; pub(crate) fn record_command(_config: &RecordingConfig, _command: &str) -> Result { - bail!("recording is only supported on Unix platforms") + bail!("recording is only supported on Linux") } pub(crate) fn record_ssh(_config: &RecordingConfig, _command: Option<&str>) -> Result { - bail!("recording is only supported on Unix platforms") + bail!("recording is only supported on Linux") } }