use super::*; use crate::{ config_client::{ClientEvent, tests::pending_close}, state::tests::unknown_board, }; use std::{io, os::fd::FromRawFd, sync::mpsc}; fn pty() -> (File, File, String) { let (mut master, mut slave) = (-1, -1); // SAFETY: valid out-pointers receive two owned fds. Optional settings are null. let opened = unsafe { libc::openpty( &mut master, &mut slave, std::ptr::null_mut(), std::ptr::null(), std::ptr::null(), ) }; assert_eq!(opened, 0); // SAFETY: openpty returned fresh fds whose ownership is transferred once. let master = unsafe { File::from_raw_fd(master) }; // SAFETY: the second fresh fd is distinct and is transferred once. let slave = unsafe { File::from_raw_fd(slave) }; // SAFETY: this live fd accepts the integer flags passed to fcntl. let configured = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) }; assert_eq!(configured, 0); let path = format!("/proc/self/fd/{}", slave.as_raw_fd()); (master, slave, path) } fn fill_transmit_queue(file: &mut File) { for _ in 0..4096 { match file.write(&[0; 4096]) { Ok(count) => assert_ne!(count, 0), Err(error) if error.kind() == io::ErrorKind::WouldBlock => return, Err(error) => panic!("cannot fill PTY: {error}"), } } panic!("PTY transmit queue did not reach its bound") } fn stall_frame(session: &mut Session, frame: MavlinkFrame) { // PTY line-discipline work can free a little space after the first EAGAIN. // Keep filling the unread peer until an actual frame write is backpressured. for _ in 0..64 { fill_transmit_queue(session.link.as_mut().unwrap()); write_raw(session, frame).unwrap(); if !flush_pending(session).unwrap() { return; } } panic!("PTY did not backpressure a configuration frame") } #[test] fn queued_save_and_reset_recheck_connection_and_configuration_before_effects() { for reconnect in [false, true] { for action in [Action::Save, Action::ResetSaved] { let state = SharedState::default(); let connection = state.connected("A".into()); state.configuration_event(ClientEvent::Configuration(unknown_board())); state.edit(401, &serde_json::json!(30)).unwrap(); let intent = state .begin_intent(state.snapshot().context, action.kind()) .unwrap(); let (_master, _slave, path) = pty(); let mut session = Session::new(Principal::new(255, 190)); session.link = Some(open_serial(&path, SerialBaud::DEFAULT).unwrap()); session.connection = Some(connection); if reconnect { state.disconnected(None); session.connection = Some(state.connected("B".into())); } state.configuration_event(ClientEvent::Configuration(unknown_board())); let before = state.snapshot(); handle_command(Command { intent, action }, &mut session, &state); assert!(session.outbound.is_none()); assert!(session.client.target().is_err()); assert_eq!(state.snapshot().config_revision, before.config_revision); assert_eq!(state.snapshot().context, before.context); assert_eq!(state.snapshot().path, before.path); } } } #[test] fn partial_writes_keep_exact_offsets_and_total_deadlines() { struct ShortWriter { bytes: Vec, stalled: bool, } impl Write for ShortWriter { fn write(&mut self, bytes: &[u8]) -> io::Result { if self.stalled { return Err(io::ErrorKind::WouldBlock.into()); } let count = bytes.len().min(3); self.bytes.extend_from_slice(&bytes[..count]); Ok(count) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } let (_, frame) = pending_close(); let mut pending = PendingWrite { frame, position: 0, deadline: Instant::now() + WRITE_TIMEOUT, }; let mut output = ShortWriter { bytes: Vec::new(), stalled: false, }; assert!(!pending.advance(&mut output).unwrap()); output.stalled = true; assert!(!pending.advance(&mut output).unwrap()); assert_eq!(pending.position, 3); output.stalled = false; for _ in 0..frame.raw_bytes().len() { if pending.advance(&mut output).unwrap() { break; } } assert_eq!(output.bytes, frame.raw_bytes()); pending.position = 0; pending.deadline = Instant::now(); assert!( pending .advance(&mut output) .unwrap_err() .contains("deadline") ); assert_eq!(output.bytes, frame.raw_bytes()); } #[test] fn nonblocking_pty_backpressure_can_be_cancelled_and_shutdown_preserves_close_uncertainty() { let (_master, _slave, path) = pty(); let mut file = open_serial(&path, SerialBaud::DEFAULT).unwrap(); // SAFETY: fcntl reads flags for this still-owned valid fd. let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) }; assert_ne!(flags & libc::O_NONBLOCK, 0); fill_transmit_queue(&mut file); let state = Arc::new(SharedState::default()); let connection = state.connected(path); state.configuration_event(ClientEvent::Configuration(unknown_board())); let (client, frame) = pending_close(); let mut session = Session::new(Principal::new(255, 190)); session.client = client; session.connection = Some(connection); session.link = Some(file); let started = Instant::now(); stall_frame(&mut session, frame); assert!(started.elapsed() < Duration::from_millis(100)); let stopping = Arc::new(AtomicBool::new(false)); let (commands, receiver) = sync_channel(COMMAND_CAPACITY); let worker_state = Arc::clone(&state); let worker_stop = Arc::clone(&stopping); let worker = thread::spawn(move || worker(receiver, worker_state, session, worker_stop)); let controller = SerialController { commands, worker: Mutex::new(Some(worker)), baud: SerialBaud::DEFAULT, state: Arc::clone(&state), stopping, }; let (done, completed) = mpsc::channel(); let shutdown = thread::spawn(move || { drop(controller); done.send(()).unwrap(); }); completed .recv_timeout(Duration::from_millis(500)) .expect("shutdown must cancel a stalled write"); shutdown.join().unwrap(); assert!(state.snapshot().path.is_none()); assert!(state.snapshot().durable_state_unknown); assert!(state.snapshot().error.unwrap().contains("unknown")); } #[test] fn stalled_pty_write_hits_its_deadline_without_blocking_the_worker() { let (_master, _slave, path) = pty(); let mut file = open_serial(&path, SerialBaud::DEFAULT).unwrap(); fill_transmit_queue(&mut file); let mut session = Session::new(Principal::new(255, 190)); session.link = Some(file); let (_, frame) = pending_close(); stall_frame(&mut session, frame); session.outbound.as_mut().unwrap().deadline = Instant::now(); assert!( flush_pending(&mut session) .unwrap_err() .contains("deadline") ); }