@@ -43,7 +43,7 @@ use tauri::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, SubmenuBuil
4343use tauri:: { AppHandle , Wry } ;
4444
4545use crate :: { get_app_handle, get_config, get_tray_id, module_alert_ui, HANDLE_CONDVAR } ;
46- use std:: io:: { BufRead , BufReader } ;
46+ use std:: io:: { BufRead , BufReader , Read } ;
4747use tauri_plugin_notification:: NotificationExt ;
4848
4949#[ derive( Debug ) ]
@@ -883,6 +883,11 @@ fn start_notify_module_thread(
883883 } )
884884 . expect ( "Failed to send module started message" ) ;
885885
886+ // Drain stderr concurrently: aw-notify logs there, and if nothing reads the pipe it fills
887+ // up (~64 KiB) and the child blocks on its next log write while we wait on stdout.
888+ let stderr = child. stderr . take ( ) . expect ( "Failed to get stderr" ) ;
889+ let stderr_reader = thread:: spawn ( move || read_tail ( stderr, STDERR_TAIL_BYTES ) ) ;
890+
886891 let stdout = child. stdout . take ( ) . expect ( "Failed to get stdout" ) ;
887892 let reader = BufReader :: new ( stdout) ;
888893
@@ -924,7 +929,13 @@ fn start_notify_module_thread(
924929 }
925930
926931 // Wait for the child to exit
927- let output = child. wait_with_output ( ) . expect ( "Failed to wait on child" ) ;
932+ let status = child. wait ( ) . expect ( "Failed to wait on child" ) ;
933+ let output = std:: process:: Output {
934+ status,
935+ // stdout was consumed line by line above
936+ stdout : Vec :: new ( ) ,
937+ stderr : stderr_reader. join ( ) . unwrap_or_default ( ) ,
938+ } ;
928939
929940 // Check if the process failed due to unsupported --output-only flag
930941 // Exit code 2 is commonly used by clap/click for argument errors
@@ -963,6 +974,33 @@ fn start_notify_module_thread(
963974 } ) ;
964975}
965976
977+ /// How much of a module's stderr to keep for crash diagnostics.
978+ const STDERR_TAIL_BYTES : usize = 64 * 1024 ;
979+
980+ /// Reads `reader` to EOF, keeping only the last `max` bytes so a chatty module can't grow memory
981+ /// without bound over a long run.
982+ fn read_tail ( mut reader : impl Read , max : usize ) -> Vec < u8 > {
983+ let mut tail = Vec :: new ( ) ;
984+ let mut buf = [ 0u8 ; 8192 ] ;
985+ loop {
986+ match reader. read ( & mut buf) {
987+ Ok ( 0 ) => break ,
988+ Ok ( n) => {
989+ tail. extend_from_slice ( & buf[ ..n] ) ;
990+ if tail. len ( ) > max {
991+ tail. drain ( ..tail. len ( ) - max) ;
992+ }
993+ }
994+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: Interrupted => continue ,
995+ Err ( e) => {
996+ warn ! ( "Failed to read module stderr: {e}" ) ;
997+ break ;
998+ }
999+ }
1000+ }
1001+ tail
1002+ }
1003+
9661004/// Route a notification: send via ManagerEvent channel (mini mode) or Tauri (GUI mode).
9671005fn route_notification ( event_tx : & Option < Sender < ManagerEvent > > , title : & str , message : & str ) {
9681006 if let Some ( tx) = event_tx {
@@ -1241,3 +1279,19 @@ fn discover_modules() -> BTreeMap<String, PathBuf> {
12411279
12421280 found_modules
12431281}
1282+
1283+ #[ cfg( test) ]
1284+ mod tests {
1285+ use super :: read_tail;
1286+
1287+ #[ test]
1288+ fn read_tail_keeps_everything_under_the_limit ( ) {
1289+ assert_eq ! ( read_tail( & b"hello" [ ..] , 64 ) , b"hello" ) ;
1290+ }
1291+
1292+ #[ test]
1293+ fn read_tail_keeps_only_the_last_bytes ( ) {
1294+ let input: Vec < u8 > = ( 0 ..20_000u32 ) . map ( |i| ( i % 251 ) as u8 ) . collect ( ) ;
1295+ assert_eq ! ( read_tail( & input[ ..] , 1000 ) , & input[ input. len( ) - 1000 ..] ) ;
1296+ }
1297+ }
0 commit comments