@@ -3,14 +3,31 @@ pub(crate) use decl::make_module;
33#[ pymodule( name = "faulthandler" ) ]
44mod decl {
55 use crate :: vm:: {
6- PyObjectRef , PyResult , VirtualMachine , frame:: Frame , function:: OptionalArg , py_io:: Write ,
6+ PyObjectRef , PyResult , VirtualMachine , builtins:: PyFloat , frame:: Frame ,
7+ function:: OptionalArg , py_io:: Write ,
78 } ;
89 use std:: sync:: atomic:: { AtomicBool , AtomicI32 , Ordering } ;
10+ use std:: sync:: { Arc , Condvar , Mutex } ;
11+ use std:: thread;
12+ use std:: time:: Duration ;
913
1014 static ENABLED : AtomicBool = AtomicBool :: new ( false ) ;
1115 #[ allow( dead_code) ]
1216 static FATAL_ERROR_FD : AtomicI32 = AtomicI32 :: new ( 2 ) ; // stderr by default
1317
18+ // Watchdog thread state for dump_traceback_later
19+ struct WatchdogState {
20+ cancel : bool ,
21+ fd : i32 ,
22+ timeout_us : u64 ,
23+ repeat : bool ,
24+ exit : bool ,
25+ header : String ,
26+ }
27+
28+ type WatchdogHandle = Arc < ( Mutex < WatchdogState > , Condvar ) > ;
29+ static WATCHDOG : Mutex < Option < WatchdogHandle > > = Mutex :: new ( None ) ;
30+
1431 // Fatal signal numbers that should use enable() instead
1532 #[ cfg( unix) ]
1633 const FATAL_SIGNALS : & [ i32 ] = & [
@@ -143,11 +160,129 @@ mod decl {
143160 ENABLED . load ( Ordering :: Relaxed )
144161 }
145162
163+ fn format_timeout ( timeout_us : u64 ) -> String {
164+ let sec = timeout_us / 1_000_000 ;
165+ let us = timeout_us % 1_000_000 ;
166+ let min = sec / 60 ;
167+ let sec = sec % 60 ;
168+ let hour = min / 60 ;
169+ let min = min % 60 ;
170+
171+ if us != 0 {
172+ format ! ( "Timeout ({:02}:{:02}:{:02}.{:06})!\n " , hour, min, sec, us)
173+ } else {
174+ format ! ( "Timeout ({:02}:{:02}:{:02})!\n " , hour, min, sec)
175+ }
176+ }
177+
178+ fn get_fd_from_file_opt ( file : OptionalArg < PyObjectRef > , vm : & VirtualMachine ) -> PyResult < i32 > {
179+ match file {
180+ OptionalArg :: Present ( f) => {
181+ // Check if it's an integer (file descriptor)
182+ if let Ok ( fd) = f. try_to_value :: < i32 > ( vm) {
183+ if fd < 0 {
184+ return Err (
185+ vm. new_value_error ( "file is not a valid file descriptor" . to_owned ( ) )
186+ ) ;
187+ }
188+ return Ok ( fd) ;
189+ }
190+ // Try to get fileno() from file object
191+ let fileno = vm. call_method ( & f, "fileno" , ( ) ) ?;
192+ let fd: i32 = fileno. try_to_value ( vm) ?;
193+ if fd < 0 {
194+ return Err (
195+ vm. new_value_error ( "file is not a valid file descriptor" . to_owned ( ) )
196+ ) ;
197+ }
198+ // Try to flush the file
199+ let _ = vm. call_method ( & f, "flush" , ( ) ) ;
200+ Ok ( fd)
201+ }
202+ OptionalArg :: Missing => {
203+ // Get sys.stderr
204+ let stderr = vm. sys_module . get_attr ( "stderr" , vm) ?;
205+ if vm. is_none ( & stderr) {
206+ return Err ( vm. new_runtime_error ( "sys.stderr is None" . to_owned ( ) ) ) ;
207+ }
208+ let fileno = vm. call_method ( & stderr, "fileno" , ( ) ) ?;
209+ let fd: i32 = fileno. try_to_value ( vm) ?;
210+ let _ = vm. call_method ( & stderr, "flush" , ( ) ) ;
211+ Ok ( fd)
212+ }
213+ }
214+ }
215+
216+ fn watchdog_thread ( state : WatchdogHandle ) {
217+ let ( lock, cvar) = & * state;
218+
219+ loop {
220+ let ( timeout, repeat, exit, fd, header, cancelled) = {
221+ let guard = lock. lock ( ) . unwrap ( ) ;
222+ if guard. cancel {
223+ return ;
224+ }
225+ (
226+ Duration :: from_micros ( guard. timeout_us ) ,
227+ guard. repeat ,
228+ guard. exit ,
229+ guard. fd ,
230+ guard. header . clone ( ) ,
231+ guard. cancel ,
232+ )
233+ } ;
234+
235+ if cancelled {
236+ return ;
237+ }
238+
239+ // Wait for timeout or cancellation
240+ let result = {
241+ let guard = lock. lock ( ) . unwrap ( ) ;
242+ cvar. wait_timeout ( guard, timeout) . unwrap ( )
243+ } ;
244+
245+ // Check if cancelled
246+ if result. 0 . cancel {
247+ return ;
248+ }
249+
250+ // Timeout occurred, dump traceback
251+ #[ cfg( not( target_arch = "wasm32" ) ) ]
252+ {
253+ let header_bytes = header. as_bytes ( ) ;
254+ unsafe {
255+ libc:: write (
256+ fd,
257+ header_bytes. as_ptr ( ) as * const libc:: c_void ,
258+ header_bytes. len ( ) ,
259+ ) ;
260+ }
261+
262+ // Note: We cannot dump actual Python traceback from a separate thread
263+ // because we don't have access to the VM's frame stack.
264+ // Just output a message indicating timeout occurred.
265+ let msg = b"<timeout: cannot dump traceback from watchdog thread>\n " ;
266+ unsafe {
267+ libc:: write ( fd, msg. as_ptr ( ) as * const libc:: c_void , msg. len ( ) ) ;
268+ }
269+
270+ if exit {
271+ std:: process:: exit ( 1 ) ;
272+ }
273+ }
274+
275+ if !repeat {
276+ return ;
277+ }
278+ }
279+ }
280+
146281 #[ derive( FromArgs ) ]
147282 #[ allow( unused) ]
148283 struct DumpTracebackLaterArgs {
149284 #[ pyarg( positional) ]
150- timeout : f64 ,
285+ timeout : PyObjectRef ,
151286 #[ pyarg( any, default = false ) ]
152287 repeat : bool ,
153288 #[ pyarg( any, default ) ]
@@ -158,18 +293,78 @@ mod decl {
158293
159294 #[ pyfunction]
160295 fn dump_traceback_later ( args : DumpTracebackLaterArgs , vm : & VirtualMachine ) -> PyResult < ( ) > {
161- // Check that file is valid
162- let _file = get_file_for_output ( args. file , vm) ?;
296+ use num_traits:: ToPrimitive ;
297+ // Convert timeout to f64 (accepting int or float)
298+ let timeout: f64 = if let Some ( float) = args. timeout . downcast_ref :: < PyFloat > ( ) {
299+ float. to_f64 ( )
300+ } else if let Some ( int) = args. timeout . try_index_opt ( vm) . transpose ( ) ? {
301+ int. as_bigint ( )
302+ . to_i64 ( )
303+ . ok_or_else ( || vm. new_overflow_error ( "timeout value is too large" . to_owned ( ) ) ) ?
304+ as f64
305+ } else {
306+ return Err ( vm. new_type_error ( "timeout must be a number (int or float)" . to_owned ( ) ) ) ;
307+ } ;
308+
309+ if timeout <= 0.0 {
310+ return Err ( vm. new_value_error ( "timeout must be greater than 0" . to_owned ( ) ) ) ;
311+ }
312+
313+ let fd = get_fd_from_file_opt ( args. file , vm) ?;
314+
315+ // Convert timeout to microseconds
316+ let timeout_us = ( timeout * 1_000_000.0 ) as u64 ;
317+ if timeout_us == 0 {
318+ return Err ( vm. new_value_error ( "timeout must be greater than 0" . to_owned ( ) ) ) ;
319+ }
320+
321+ let header = format_timeout ( timeout_us) ;
322+
323+ // Cancel any previous watchdog
324+ cancel_dump_traceback_later ( ) ;
163325
164- // TODO: Implement watchdog thread that dumps traceback after timeout
165- // For now, this is a stub
166- Err ( vm. new_not_implemented_error ( "dump_traceback_later is not yet implemented" ) )
326+ // Create new watchdog state
327+ let state = Arc :: new ( (
328+ Mutex :: new ( WatchdogState {
329+ cancel : false ,
330+ fd,
331+ timeout_us,
332+ repeat : args. repeat ,
333+ exit : args. exit ,
334+ header,
335+ } ) ,
336+ Condvar :: new ( ) ,
337+ ) ) ;
338+
339+ // Store the state
340+ {
341+ let mut watchdog = WATCHDOG . lock ( ) . unwrap ( ) ;
342+ * watchdog = Some ( Arc :: clone ( & state) ) ;
343+ }
344+
345+ // Start watchdog thread
346+ thread:: spawn ( move || {
347+ watchdog_thread ( state) ;
348+ } ) ;
349+
350+ Ok ( ( ) )
167351 }
168352
169353 #[ pyfunction]
170354 fn cancel_dump_traceback_later ( ) {
171- // TODO: Cancel the watchdog thread
172- // For now, this is a no-op since dump_traceback_later is not implemented
355+ let state = {
356+ let mut watchdog = WATCHDOG . lock ( ) . unwrap ( ) ;
357+ watchdog. take ( )
358+ } ;
359+
360+ if let Some ( state) = state {
361+ let ( lock, cvar) = & * state;
362+ {
363+ let mut guard = lock. lock ( ) . unwrap ( ) ;
364+ guard. cancel = true ;
365+ }
366+ cvar. notify_all ( ) ;
367+ }
173368 }
174369
175370 #[ cfg( unix) ]
0 commit comments