Visitar URL original
fix watchdog · RustPython/RustPython@7e73e44 · GitHub
Skip to content

Commit 7e73e44

Browse files
committed
fix watchdog
1 parent 41d8327 commit 7e73e44

1 file changed

Lines changed: 204 additions & 9 deletions

File tree

‎crates/stdlib/src/faulthandler.rs‎

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,31 @@ pub(crate) use decl::make_module;
33
#[pymodule(name = "faulthandler")]
44
mod 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

Comments
 (0)