char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7b01146
main
1#! [ cfg ( unix )] 2 3# [ path = "common/registry.rs" ] 4mod fixture; 5 6use std:: fs:: File ; 7use std:: io::{ Read , Write }; 8use std:: os:: fd::{ AsRawFd , FromRawFd , OwnedFd }; 9use std:: os:: unix:: process:: CommandExt ; 10use std:: process::{ Child , Command , Stdio }; 11use std:: sync:: Mutex ; 12use std:: sync:: atomic:: Ordering ; 13use std:: thread; 14use std:: time::{ Duration , Instant }; 15 16use fixture:: Registry ; 17 18const DEADLINE : Duration =Duration :: from_secs ( 10 ); 19 20struct Pty { 21child : Child , 22master : File , 23bytes : Vec < u8 >, 24cursor_scan : usize , 25eof : bool , 26} 27 28impl Pty { 29fn spawn ( args : & [ String ], directory : & std:: path:: Path ) ->Self { 30let winsize = libc:: winsize { 31ws_row : 24 , 32ws_col : 80 , 33ws_xpixel : 0 , 34ws_ypixel : 0 , 35}; 36let ( mut master, mut slave) =( -1 , -1 ); 37assert_eq! ( 38unsafe { 39 libc:: openpty ( 40& mut master, 41& mut slave, 42 std:: ptr:: null_mut (), 43 std:: ptr:: null (), 44& winsize, 45) 46}, 470 48); 49let master =unsafe { OwnedFd :: from_raw_fd ( master) }; 50let slave =unsafe { OwnedFd :: from_raw_fd ( slave) }; 51for fdin [ & master, & slave] { 52let flags =unsafe { libc:: fcntl ( fd. as_raw_fd (), libc:: F_GETFD ) }; 53assert! ( flags >=0 ); 54assert_eq! ( 55unsafe { libc:: fcntl ( fd. as_raw_fd (), libc:: F_SETFD , flags | libc:: FD_CLOEXEC ) }, 560 57); 58} 59 60let mut command =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )); 61 command 62. args ( args) 63. current_dir ( directory) 64. env ( "TERM" , "xterm" ) 65. env ( "NO_COLOR" , "1" ) 66. stdin ( Stdio :: from ( slave. try_clone (). unwrap ())) 67. stdout ( Stdio :: from ( slave. try_clone (). unwrap ())) 68. stderr ( Stdio :: from ( slave. try_clone (). unwrap ())); 69unsafe { 70 command. pre_exec ( ||{ 71if libc:: setsid () <0 || libc:: ioctl ( libc:: STDIN_FILENO , libc:: TIOCSCTTY , 0 ) <0 { 72return Err ( std:: io:: Error :: last_os_error ()); 73} 74 libc:: signal ( libc:: SIGINT , libc:: SIG_DFL ); 75 libc:: signal ( libc:: SIGTERM , libc:: SIG_DFL ); 76Ok (()) 77}); 78} 79let child = command. spawn (). unwrap (); 80drop ( slave); 81Self { 82 child, 83master : File :: from ( master), 84bytes : Vec :: new (), 85cursor_scan : 0 , 86eof : false , 87} 88} 89 90fn write ( & mut self , bytes : & [ u8 ]) { 91self . master . write_all ( bytes). unwrap (); 92} 93 94fn read_until ( & mut self , needle : & [ u8 ]) ->usize { 95self . read_until_from ( 0 , needle) 96} 97 98fn read_until_from ( & mut self , start : usize , needle : & [ u8 ]) ->usize { 99let deadline =Instant :: now () +DEADLINE ; 100loop { 101if let Some ( position) =self . bytes [ start..] 102. windows ( needle. len ()) 103. position ( |w| w == needle) 104{ 105return start + position + needle. len (); 106} 107assert! ( 108Instant :: now () < deadline, 109"timed out waiting for {:?}; got {:?}" , 110String :: from_utf8_lossy ( needle), 111String :: from_utf8_lossy ( & self . bytes ) 112); 113assert! ( 114self . read_more ( deadline), 115"PTY closed before {:?}: {}" , 116String :: from_utf8_lossy ( needle), 117String :: from_utf8_lossy ( & self . bytes ) 118); 119} 120} 121 122fn read_more ( & mut self , deadline : Instant ) ->bool { 123if self . eof { 124return false ; 125} 126let mut poll = libc:: pollfd { 127fd : self . master . as_raw_fd (), 128events : libc:: POLLIN , 129revents : 0 , 130}; 131let timeout = deadline 132. saturating_duration_since ( Instant :: now ()) 133. as_millis () 134. min ( i32:: MAX as u128 ) as i32 ; 135let ready =unsafe { libc:: poll ( & mut poll, 1 , timeout) }; 136if ready <0 && std:: io:: Error :: last_os_error (). kind () == std:: io:: ErrorKind :: Interrupted { 137return true ; 138} 139assert! ( 140 ready >=0 , 141"PTY poll failed: {}" , 142 std:: io:: Error :: last_os_error () 143); 144if ready ==0 { 145return false ; 146} 147let mut buffer =[ 0u8 ; 4096 ]; 148match self . master . read ( & mut buffer) { 149Ok ( 0 ) =>self . eof =true , 150Ok ( count) =>{ 151self . bytes . extend_from_slice ( & buffer[ ..count]); 152self . answer_cursor_queries (); 153return true ; 154} 155Err ( error) if error. raw_os_error () ==Some ( libc:: EIO ) =>self . eof =true , 156Err ( error) if error. kind () == std:: io:: ErrorKind :: Interrupted =>return true , 157Err ( error) =>panic! ( "PTY read failed: {error}" ), 158} 159false 160} 161 162fn answer_cursor_queries ( & mut self ) { 163while let Some ( position) =self . bytes [ self . cursor_scan ..] 164. windows ( 4 ) 165. position ( |w| w ==b"\x1b[6n" ) 166{ 167self . cursor_scan += position +4 ; 168self . write ( b"\x1b[1;1R" ); 169} 170} 171 172fn wait_exit ( & mut self ) { 173let deadline =Instant :: now () +DEADLINE ; 174loop { 175if let Some ( status) =self . child . try_wait (). unwrap () { 176while self . read_more ( Instant :: now ()) {} 177assert! ( 178 status. success (), 179"child exited {status}: {}" , 180String :: from_utf8_lossy ( & self . bytes ) 181); 182return ; 183} 184assert! ( Instant :: now () < deadline, "child did not exit" ); 185self . read_more ( deadline); 186 thread:: yield_now (); 187} 188} 189} 190 191impl Drop for Pty { 192fn drop ( & mut self ) { 193if self . child . try_wait (). ok (). flatten (). is_none () { 194let _ =self . child . kill (); 195let _ =self . child . wait (); 196} 197} 198} 199 200fn wait_for ( observed : & Mutex < Vec < String >>, path : & str ) { 201let deadline =Instant :: now () +DEADLINE ; 202while !observed. lock (). unwrap (). iter (). any ( |item| item == path) { 203assert! ( Instant :: now () < deadline, "server did not record {path}" ); 204 thread:: yield_now (); 205} 206} 207 208# [ test ] 209fn interrupted_repl_suppresses_distinct_late_http_progress () { 210let server =Registry :: new (); 211 server. concurrent . store ( true , Ordering :: Relaxed ); 212 server. files . lock (). unwrap (). insert ( 213"/mod.ts" . into (), 214b"import './helper.ts'; export const value = 42;" . to_vec (), 215); 216 server. files . lock (). unwrap (). insert ( 217"/helper.ts" . into (), 218b"console.log('LATE_HELPER_COMPLETE'); export {};" . to_vec (), 219); 220 server 221. hold_paths 222. lock () 223. unwrap () 224. extend ([ "/mod.ts" . into (), "/helper.ts" . into ()]); 225let directory = tempfile:: tempdir (). unwrap (); 226let mut child =Pty :: spawn ( 227& [ 228"repl" . into (), 229format! ( "--cache-dir={}" , directory. path (). join ( "cache" ). display ()), 230], 231 directory. path (), 232); 233 child. read_until ( b"> " ); 234// Keep module names out of echoed input so it cannot masquerade as progress. 235 child. write ( 236format! ( 237"await (await import('{}' + 'mod' + '.ts')).value\n" , 238 server. url 239) 240. as_bytes (), 241); 242wait_for ( & server. requests , "/mod.ts" ); 243let progress = child. read_until ( b"mod.ts" ); 244assert_eq! ( 245unsafe { libc:: kill ( child. child . id () as i32 , libc:: SIGINT ) }, 2460 247); 248let prompt = child. read_until_from ( progress, b"> " ); 249 server. hold_paths . lock (). unwrap (). remove ( "/mod.ts" ); 250wait_for ( & server. requests , "/helper.ts" ); 251 server. hold_paths . lock (). unwrap (). remove ( "/helper.ts" ); 252wait_for ( & server. responses , "/mod.ts" ); 253wait_for ( & server. responses , "/helper.ts" ); 254 255let barrier = child. bytes . len (); 256 child. write ( 257format! ( 258"await import('{}' + 'helper' + '.ts'); ['PROGRESS', 'BARRIER'].join('_')\n" , 259 server. url 260) 261. as_bytes (), 262); 263 child. read_until_from ( prompt, b"LATE_HELPER_COMPLETE" ); 264let completion = child. read_until_from ( barrier, b"PROGRESS_BARRIER" ); 265 child. read_until_from ( completion, b"> " ); 266let idle_output =String :: from_utf8_lossy ( & child. bytes [ prompt..]); 267for targetin [ "mod.ts" , "helper.ts" ] { 268assert! ( 269 !idle_output. contains ( target), 270"late progress escaped scope: {idle_output}" 271); 272} 273 child. write ( b".exit\n" ); 274 child. wait_exit (); 275assert! ( !directory. path (). join ( "avril.lock" ). exists ()); 276}