diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ffc9add..d25ea117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [Windows] Fixed a crash in monitor when espflash is connected via USB Serial/JTAG, and the user is typing into the monitor but the device is not reading serial input. (#943) +- [Linux] Fixed espflash hanging when espflash is connected via USB Serial/JTAG, and the user is typing into the monitor but the device is not reading serial input. (#944) ### Removed diff --git a/Cargo.lock b/Cargo.lock index 153d7e33..484b8d58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,11 +241,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" -version = "1.1.12" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ "find-msvc-tools", "shlex", @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.2.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8e18d0dca9578507f13f9803add0df13362b02c501c1c17734f0dbb52eaf0b" +checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" dependencies = [ "crossterm", "unicode-segmentation", @@ -900,11 +900,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" dependencies = [ "serde", + "serde_core", "typeid", ] @@ -960,6 +961,7 @@ dependencies = [ "log", "md-5", "miette", + "nix 0.30.1", "object 0.37.3", "regex", "serde", @@ -1272,9 +1274,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1533,9 +1535,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", @@ -2218,9 +2220,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" dependencies = [ "ring", "rustls-pki-types", @@ -2267,30 +2269,33 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-untagged" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", + "serde_core", "typeid", ] @@ -2304,11 +2309,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.223" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56" dependencies = [ "proc-macro2", "quote", @@ -2317,14 +2331,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -3170,13 +3185,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -3217,20 +3232,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml index e2a49a6a..ed8f3b1d 100644 --- a/espflash/Cargo.toml +++ b/espflash/Cargo.toml @@ -62,6 +62,9 @@ update-informer = { version = "1.2", optional = true } [target.'cfg(unix)'.dependencies] libc = "0.2.174" +[target.'cfg(target_os = "linux")'.dependencies] +nix = { version = "0.30", features = ["time"] } + [features] default = ["cli"] cli = [ diff --git a/espflash/src/cli/monitor/mod.rs b/espflash/src/cli/monitor/mod.rs index ceecfa1b..158f4589 100644 --- a/espflash/src/cli/monitor/mod.rs +++ b/espflash/src/cli/monitor/mod.rs @@ -12,7 +12,7 @@ use std::{ io::{self, ErrorKind, Read, Write, stdout}, - time::Duration, + time::{Duration, Instant}, }; use crossterm::{ @@ -130,6 +130,7 @@ pub fn monitor( ExternalProcessors::new(monitor_args.processors, monitor_args.elf)?; let mut buff = [0; 1024]; + let mut user_input_handler = InputHandler::new(pid, non_interactive); loop { let read_count = match serial.read(&mut buff) { Ok(count) => Ok(count), @@ -144,7 +145,7 @@ pub fn monitor( // Don't forget to flush the writer! stdout.flush().ok(); - if !handle_user_input(&mut serial, pid, non_interactive)? { + if !user_input_handler.handle(&mut serial)? { break; } } @@ -152,40 +153,143 @@ pub fn monitor( Ok(()) } -/// Handle user input from the terminal. -/// -/// Returns `true` if the program should continue running, `false` if it should -/// exit. -fn handle_user_input(serial: &mut Port, pid: u16, non_interactive: bool) -> Result { - let key = match key_event().into_diagnostic() { - Ok(Some(event)) => event, - Ok(None) => return Ok(true), - Err(_) if non_interactive => return Ok(true), - Err(err) => return Err(err), - }; +struct InputHandler { + pid: u16, + non_interactive: bool, + flush_deadline: Option, +} + +impl InputHandler { + fn new(pid: u16, non_interactive: bool) -> Self { + Self { + pid, + non_interactive, + flush_deadline: None, + } + } + + fn flush_if_needed(&mut self, serial: &mut Port) -> Result<()> { + let Some(deadline) = self.flush_deadline else { + return Ok(()); + }; + + if deadline <= Instant::now() { + self.flush_deadline = None; + #[cfg(target_os = "linux")] + let _timer = linux::arm_timeout_workaround(Duration::from_millis(100)); + serial.flush().ignore_timeout().into_diagnostic()?; + } + + Ok(()) + } + + /// Handle user input from the terminal. + /// + /// Returns `true` if the program should continue running, `false` if it + /// should exit. + fn handle(&mut self, serial: &mut Port) -> Result { + let key = match key_event().into_diagnostic() { + Ok(Some(event)) => event, + Ok(None) => { + self.flush_if_needed(serial)?; + return Ok(true); + } + Err(_) if self.non_interactive => return Ok(true), + Err(err) => return Err(err), + }; + + if key.kind == KeyEventKind::Press { + if key.modifiers.contains(KeyModifiers::CONTROL) { + match key.code { + KeyCode::Char('c') => return Ok(false), + KeyCode::Char('r') => { + reset_after_flash(serial, self.pid).into_diagnostic()?; + return Ok(true); + } + _ => {} + } + } - if key.kind == KeyEventKind::Press { - if key.modifiers.contains(KeyModifiers::CONTROL) { - match key.code { - KeyCode::Char('c') => return Ok(false), - KeyCode::Char('r') => { - reset_after_flash(serial, pid).into_diagnostic()?; - return Ok(true); + self.flush_if_needed(serial)?; + if let Some(bytes) = handle_key_event(key) { + serial + .write_all(&bytes) + .ignore_timeout() + .into_diagnostic()?; + if self.flush_deadline.is_none() { + self.flush_deadline = Some(Instant::now() + Duration::from_millis(50)); } - _ => {} } } - if let Some(bytes) = handle_key_event(key) { - serial - .write_all(&bytes) - .ignore_timeout() - .into_diagnostic()?; - serial.flush().ignore_timeout().into_diagnostic()?; + Ok(true) + } +} + +#[cfg(target_os = "linux")] +mod linux { + use std::time::Duration; + + use nix::{ + sys::{ + signal::{self, SaFlags, SigAction, SigEvent, SigHandler, SigSet, SigevNotify, Signal}, + timer::{Expiration, Timer, TimerSetTimeFlags}, + }, + time::ClockId, + }; + pub struct Workaround { + _timer: Timer, + previous_handler: SigHandler, + } + + const SIGNAL: Signal = Signal::SIGALRM; + + impl Drop for Workaround { + fn drop(&mut self) { + unsafe { signal::signal(SIGNAL, self.previous_handler) }.unwrap(); } } - Ok(true) + /// Sets a timer that will send a signal to interrupt the IO drain if it + /// doesn't complete in the given amount of time. + /// + /// The timer is cancelled if the returned object is dropped (which happens + /// after the IO operation returns). Implementation based on the second idea + /// in https://stackoverflow.com/a/29307379 - as setting the O_NONBLOCK flag + /// doesn't seem to work for us. + pub fn arm_timeout_workaround(timeout: Duration) -> Workaround { + extern "C" fn handle_signal(_signal: libc::c_int) {} + + // Register signal handler to prevent killing the program. The original handler + // will be restored during cleanup. + let handler = SigHandler::Handler(handle_signal); + unsafe { + signal::sigaction( + SIGNAL, + &SigAction::new(handler, SaFlags::empty(), SigSet::all()), + ) + .unwrap() + }; + let previous_handler = unsafe { signal::signal(SIGNAL, handler) }.unwrap(); + + // Start a one-shot timer to raise our signal. + let mut timer = Timer::new( + ClockId::CLOCK_MONOTONIC, + SigEvent::new(SigevNotify::SigevSignal { + signal: SIGNAL, + si_value: 0, + }), + ) + .unwrap(); + let expiration = Expiration::OneShot(timeout.into()); + let flags = TimerSetTimeFlags::empty(); + timer.set(expiration, flags).expect("could not set timer"); + + Workaround { + _timer: timer, + previous_handler, + } + } } trait ErrorExt {