Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions crates/vm/src/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> {
.signal_handlers
.get()
.expect("should never fail since we check above");
let frame = crate::frame::current_complete_frame_for_trace(vm);

for (signum, trigger) in TRIGGERS.iter().enumerate().skip(1) {
let triggered = trigger.swap(false, Ordering::Relaxed);
Expand All @@ -155,10 +156,19 @@ fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> {
// mutably, and a live read borrow turns that into a panic.
let handler = signal_handlers.borrow()[signum].clone();

if let Some(handler) = handler
&& let Some(callable) = handler.to_callable()
&& let Err(exc) = callable.invoke((signum.as_i32(), vm.ctx.none()), vm)
{
let Some(handler) = handler.filter(|handler| !vm.is_none(handler)) else {
continue;
};
#[cfg(feature = "host_env")]
if crate::stdlib::_signal::_signal::handler_disposition(&handler, vm).is_some() {
continue;
}

// A handler can cease to be callable after registration. Invoke it
// normally so that this raises TypeError instead of losing a signal.
// Keep the interrupted complete frame alive across every callback,
// including handlers which re-enter signal dispatch.
if let Err(exc) = handler.call((signum.as_i32(), frame.clone()), vm) {
// On error, re-schedule a call to trigger_signals() for the
// remaining signals and the pending wakeup errors.
set_triggered();
Expand Down
161 changes: 104 additions & 57 deletions crates/vm/src/stdlib/_signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ pub(crate) mod _signal {

use crate::{
Py, PyObjectRef, PyResult, VirtualMachine,
function::ArgIntoBool,
signal::{self, SignalHandlers, SignalNum},
};
use core::sync::atomic::{self, Ordering};

cfg_select! {
any(unix, windows, target_os = "wasi") => {
use crate::convert::{IntoPyException, TryFromBorrowedObject};
use crate::convert::IntoPyException;
use rustpython_host_env::signal as host_signal;
}
_ => {}
Expand All @@ -24,9 +25,9 @@ pub(crate) mod _signal {
unix => {
use crate::{
builtins::{PyBaseExceptionRef, PyTypeRef},
function::ArgIntoFloat,
stdlib::time::pytime::{self, Round},
};
use rustpython_host_env::signal::{double_to_timeval, itimerval_to_tuple};
use rustpython_host_env::signal::itimerval_to_tuple;

use std::os::fd::AsFd;
}
Expand All @@ -41,6 +42,8 @@ pub(crate) mod _signal {

cfg_select! {
windows => {
use crate::convert::TryFromBorrowedObject;

type WakeupFdRaw = libc::SOCKET;
struct WakeupFd(WakeupFdRaw);
const INVALID_WAKEUP: libc::SOCKET = host_signal::INVALID_SOCKET;
Expand All @@ -55,19 +58,9 @@ pub(crate) mod _signal {
vm: &VirtualMachine,
obj: &'a crate::PyObject,
) -> PyResult<Self> {
use num_traits::One;

let fd: &crate::Py<crate::builtins::PyInt> = obj.try_to_value(vm)?;
match fd.try_to_primitive::<usize>(vm) {
Ok(fd) => Ok(Self(fd as _)),
Err(e) => {
if (-fd.as_bigint()).is_one() {
Ok(Self(INVALID_WAKEUP))
} else {
Err(e)
}
}
}
// PyLong_AsSocket_t converts through a signed pointer-sized
// integer, including __index__, before casting to SOCKET.
isize::try_from_borrowed_object(vm, obj).map(|fd| Self(fd as _))
}
}
}
Expand Down Expand Up @@ -166,8 +159,12 @@ pub(crate) mod _signal {
}

#[cfg(unix)]
fn new_itimer_error(msg: &str, vm: &VirtualMachine) -> PyBaseExceptionRef {
vm.new_os_subtype_error(itimer_error(vm), None, msg)
fn new_itimer_error(err: std::io::Error, vm: &VirtualMachine) -> PyBaseExceptionRef {
let errno = err.raw_os_error();
let message = errno
.and_then(rustpython_host_env::errno::strerror_string)
.unwrap_or_else(|| err.to_string());
vm.new_os_subtype_error(itimer_error(vm), errno, message)
.upcast()
}

Expand Down Expand Up @@ -207,19 +204,41 @@ pub(crate) mod _signal {
// SAFETY: Trust `SignalNum::VALID_RANGE`
let signum = unsafe { SignalNum::new_unchecked(signum) };

vm.signal_handlers
.get_or_init(SignalHandlers::default)
.borrow_mut()[signum] = py_handler;
let signal_handlers = vm.signal_handlers.get_or_init(SignalHandlers::default);
let _old_handler =
core::mem::replace(&mut signal_handlers.borrow_mut()[signum], py_handler);
// Replacing a handler can run its destructor, which may install
// another handler. Release the table borrow before dropping it.
}

let int_handler = module
.get_attr("default_int_handler", vm)
.expect("_signal does not have this attr?");

signal(SignalNum::SIGINT, int_handler, vm).expect("Failed to set sigint handler");
let default_sigint = vm
.signal_handlers
.get()
.and_then(|handlers| handlers.borrow()[SignalNum::SIGINT].clone())
.is_some_and(|handler| handler_disposition(&handler, vm) == Some(SIG_DFL));
if default_sigint {
let int_handler = module
.get_attr("default_int_handler", vm)
.expect("_signal does not have this attr?");

// An inherited ignored or foreign native handler is not ours
// to replace, even when installing Python's default handlers.
signal(SignalNum::SIGINT.into(), int_handler, vm)
.expect("Failed to set sigint handler");
}
}
}

pub(crate) fn handler_disposition(
handler: &crate::PyObject,
vm: &VirtualMachine,
) -> Option<sighandler_t> {
handler
.downcast_ref_if_exact::<crate::builtins::PyInt>(vm)
.and_then(|value| usize::try_from(value.as_bigint()).ok())
.filter(|&value| matches!(value, SIG_DFL | SIG_IGN))
}

#[cfg(not(any(unix, windows, target_os = "wasi")))]
#[pyfunction]
pub fn signal(
Expand All @@ -233,16 +252,26 @@ pub(crate) mod _signal {
#[cfg(any(unix, windows, target_os = "wasi"))]
#[pyfunction]
pub fn signal(
signalnum: SignalNum,
signalnum: i32,
handler: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<Option<PyObjectRef>> {
if !vm.is_main_thread() {
#[cfg(windows)]
if !matches!(
signalnum,
SIGABRT | SIGBREAK | SIGFPE | SIGILL | SIGINT | SIGSEGV | SIGTERM
) {
return Err(vm.new_value_error("invalid signal value"));
}

if !vm.is_main_thread() || !vm.state.is_main_interpreter() {
return Err(
vm.new_value_error("signal only works in main thread of the main interpreter")
);
}

let signalnum = SignalNum::try_from(signalnum).map_err(|msg| vm.new_value_error(msg))?;

let sig_handler = if handler.is_callable() {
let callback = run_signal as *const () as sighandler_t;
cfg_select! {
Expand All @@ -253,10 +282,8 @@ pub(crate) mod _signal {
const MSG: &str =
"signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object";

let disposition = usize::try_from_borrowed_object(vm, &handler)
.ok()
.filter(|&v| matches!(v, SIG_DFL | SIG_IGN))
.ok_or_else(|| vm.new_type_error(MSG))?;
let disposition =
handler_disposition(&handler, vm).ok_or_else(|| vm.new_type_error(MSG))?;
cfg_select! {
target_os = "wasi" => {
if disposition == SIG_DFL {
Expand Down Expand Up @@ -289,8 +316,8 @@ pub(crate) mod _signal {

#[cfg(unix)]
#[pyfunction]
fn alarm(seconds: u32) -> u32 {
rustpython_host_env::signal::alarm(seconds)
fn alarm(seconds: i32) -> u32 {
rustpython_host_env::signal::alarm(seconds as u32)
}

#[cfg(unix)]
Expand All @@ -307,9 +334,9 @@ pub(crate) mod _signal {
#[pyarg(positional)]
which: i32,
#[pyarg(positional)]
seconds: ArgIntoFloat,
#[pyarg(positional, default = 0.0)]
interval: ArgIntoFloat,
seconds: PyObjectRef,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if this is necessary after #9563

#[pyarg(positional, optional)]
interval: crate::function::OptionalArg<PyObjectRef>,
}

#[cfg(unix)]
Expand All @@ -320,50 +347,70 @@ pub(crate) mod _signal {
seconds,
interval,
} = args;
let seconds: f64 = seconds.into();
let interval: f64 = interval.into();
let it_value = timer_timeval(&seconds, vm)?;
let it_interval = match interval {
crate::function::OptionalArg::Present(interval) => timer_timeval(&interval, vm)?,
crate::function::OptionalArg::Missing => libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
};
let new = libc::itimerval {
it_value: double_to_timeval(seconds),
it_interval: double_to_timeval(interval),
it_value,
it_interval,
};
host_signal::setitimer(which, &new)
.map(|old| itimerval_to_tuple(&old))
.map_err(|err| new_itimer_error(&err.to_string(), vm))
.map_err(|err| new_itimer_error(err, vm))
}

#[cfg(unix)]
fn timer_timeval(object: &crate::PyObject, vm: &VirtualMachine) -> PyResult<libc::timeval> {
let nanoseconds = pytime::from_seconds_object(object, Round::Ceiling, vm)?;
let microseconds = Round::Ceiling.divide(nanoseconds, 1_000);
let seconds = microseconds.div_euclid(1_000_000);
#[allow(
clippy::useless_conversion,
reason = "time_t may be narrower on 32-bit targets"
)]
let tv_sec = seconds
.try_into()
.map_err(|_| vm.new_overflow_error("timestamp out of range for platform time_t"))?;
Ok(libc::timeval {
tv_sec,
tv_usec: microseconds.rem_euclid(1_000_000) as _,
})
}

#[cfg(unix)]
#[pyfunction]
fn getitimer(which: i32, vm: &VirtualMachine) -> PyResult<(f64, f64)> {
host_signal::getitimer(which)
.map(|old| itimerval_to_tuple(&old))
.map_err(|err| new_itimer_error(&err.to_string(), vm))
.map_err(|err| new_itimer_error(err, vm))
}

#[pyfunction]
fn default_int_handler(
_signalnum: PyObjectRef,
_frame: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult {
fn default_int_handler(_signalnum: i32, _frame: PyObjectRef, vm: &VirtualMachine) -> PyResult {
Err(vm.new_exception_empty(vm.ctx.exceptions.keyboard_interrupt.to_owned()))
}

#[derive(FromArgs)]
struct SetWakeupFdArgs {
#[pyarg(positional)]
fd: WakeupFd,
#[pyarg(named, default = true)]
warn_on_full_buffer: bool,
fd: PyObjectRef,
#[pyarg(named, default = ArgIntoBool::TRUE)]
warn_on_full_buffer: ArgIntoBool,
}

#[pyfunction]
fn set_wakeup_fd(args: SetWakeupFdArgs, vm: &VirtualMachine) -> PyResult<i64> {
let fd = cfg_select! {
windows => args.fd.0,
_ => args.fd,
};
// The keyword truth value is parsed before the fd conversion in CPython.
let fd = args.fd.try_to_value::<WakeupFd>(vm)?;
#[cfg(windows)]
let fd = fd.0;

if !vm.is_main_thread() {
if !vm.is_main_thread() || !vm.state.is_main_interpreter() {
return Err(vm.new_value_error(
"set_wakeup_fd only works in main thread of the main interpreter",
));
Expand Down Expand Up @@ -393,7 +440,7 @@ pub(crate) mod _signal {
}

let old_fd = WAKEUP.swap(fd, Ordering::Relaxed);
WARN_ON_FULL_BUFFER.store(args.warn_on_full_buffer, Ordering::Relaxed);
WARN_ON_FULL_BUFFER.store(args.warn_on_full_buffer.into_bool(), Ordering::Relaxed);

#[cfg(windows)]
WAKEUP_IS_SOCKET.store(is_socket, Ordering::Relaxed);
Expand Down
Loading