mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
impl sys.monitoring
This commit is contained in:
1 parent
f35d0fde0b
commit
739f92e872
8 files changed
+1219
-31
No files matched your search
Vendored
-2
@@ -1012,8 +1012,6 @@ if 1:
|
||||
code_lines = self.get_code_lines(genexp_code)
|
||||
self.assertEqual(genexp_lines, code_lines)
|
||||
|
||||
# TODO: RUSTPYTHON; implicit return line number after async for
|
||||
@unittest.expectedFailure
|
||||
def test_line_number_implicit_return_after_async_for(self):
|
||||
|
||||
async def test(aseq):
|
||||
|
||||
@@ -2865,6 +2865,9 @@ impl Compiler {
|
||||
// Normal path jumps here to skip exception path blocks
|
||||
let end_block = self.new_block();
|
||||
|
||||
// Emit NOP at the try: line so LINE events fire for it
|
||||
emit!(self, Instruction::Nop);
|
||||
|
||||
// Setup a finally block if we have a finally statement.
|
||||
// Push fblock with handler info for exception table generation
|
||||
// IMPORTANT: handler goes to finally_except_block (exception path), not finally_block
|
||||
@@ -3018,8 +3021,10 @@ impl Compiler {
|
||||
type_,
|
||||
name,
|
||||
body,
|
||||
range: handler_range,
|
||||
..
|
||||
}) = &handler;
|
||||
self.set_source_range(*handler_range);
|
||||
let next_handler = self.new_block();
|
||||
|
||||
// If we gave a typ,
|
||||
@@ -3313,6 +3318,9 @@ impl Compiler {
|
||||
};
|
||||
let exit_block = self.new_block();
|
||||
|
||||
// Emit NOP at the try: line so LINE events fire for it
|
||||
emit!(self, Instruction::Nop);
|
||||
|
||||
// Push fblock with handler info for exception table generation
|
||||
if !finalbody.is_empty() {
|
||||
emit!(
|
||||
@@ -3743,6 +3751,9 @@ impl Compiler {
|
||||
is_async: bool,
|
||||
funcflags: bytecode::MakeFunctionFlags,
|
||||
) -> CompileResult<()> {
|
||||
// Save source range so MAKE_FUNCTION gets the `def` line, not the body's last line
|
||||
let saved_range = self.current_source_range;
|
||||
|
||||
// Always enter function scope
|
||||
self.enter_function(name, parameters)?;
|
||||
self.current_code_info()
|
||||
@@ -3796,6 +3807,9 @@ impl Compiler {
|
||||
let code = self.exit_scope();
|
||||
self.ctx = prev_ctx;
|
||||
|
||||
// Restore source range so MAKE_FUNCTION is attributed to the `def` line
|
||||
self.set_source_range(saved_range);
|
||||
|
||||
// Create function object with closure
|
||||
self.make_closure(code, funcflags)?;
|
||||
|
||||
@@ -5169,15 +5183,22 @@ impl Compiler {
|
||||
if is_async {
|
||||
emit!(self, Instruction::EndAsyncFor);
|
||||
} else {
|
||||
// END_FOR + POP_ITER pattern (CPython 3.14)
|
||||
// FOR_ITER jumps to END_FOR, but VM skips it (+1) to reach POP_ITER
|
||||
// END_FOR + POP_ITER are on the `for` line, not the body's last line
|
||||
let saved_range = self.current_source_range;
|
||||
self.set_source_range(iter.range());
|
||||
emit!(self, Instruction::EndFor);
|
||||
emit!(self, Instruction::PopIter);
|
||||
self.set_source_range(saved_range);
|
||||
}
|
||||
self.compile_statements(orelse)?;
|
||||
|
||||
self.switch_to_block(after_block);
|
||||
|
||||
// Restore source range to the `for` line so any implicit return
|
||||
// (LOAD_CONST None, RETURN_VALUE) is attributed to the `for` line,
|
||||
// not the loop body's last line.
|
||||
self.set_source_range(iter.range());
|
||||
|
||||
self.leave_conditional_block();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -250,15 +250,33 @@ impl CodeInfo {
|
||||
let mut locations = Vec::new();
|
||||
let mut linetable_locations: Vec<LineTableLocation> = Vec::new();
|
||||
|
||||
// Convert pseudo ops and remove resulting NOPs
|
||||
// Convert pseudo ops and remove resulting NOPs (keep line-marker NOPs)
|
||||
convert_pseudo_ops(&mut blocks, varname_cache.len() as u32);
|
||||
for block in blocks
|
||||
.iter_mut()
|
||||
.filter(|b| b.next != BlockIdx::NULL || !b.instructions.is_empty())
|
||||
{
|
||||
block
|
||||
// Collect lines that have non-NOP instructions in this block
|
||||
let non_nop_lines: std::collections::HashSet<_> = block
|
||||
.instructions
|
||||
.retain(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)));
|
||||
.iter()
|
||||
.filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)))
|
||||
.map(|ins| ins.location.line)
|
||||
.collect();
|
||||
let mut kept_nop_lines: std::collections::HashSet<OneIndexed> =
|
||||
std::collections::HashSet::new();
|
||||
block.instructions.retain(|ins| {
|
||||
if matches!(ins.instr.real(), Some(Instruction::Nop)) {
|
||||
let line = ins.location.line;
|
||||
// Remove if another instruction covers this line,
|
||||
// or if we already kept a NOP for this line
|
||||
if non_nop_lines.contains(&line) || kept_nop_lines.contains(&line) {
|
||||
return false;
|
||||
}
|
||||
kept_nop_lines.insert(line);
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
let mut block_to_offset = vec![Label(0); blocks.len()];
|
||||
@@ -501,9 +519,14 @@ impl CodeInfo {
|
||||
let tuple_const = ConstantData::Tuple { elements };
|
||||
let (const_idx, _) = self.metadata.consts.insert_full(tuple_const);
|
||||
|
||||
// Replace preceding LOAD instructions with NOP
|
||||
// Replace preceding LOAD instructions with NOP, using the
|
||||
// BUILD_TUPLE location so remove_nops() treats them as
|
||||
// same-line and removes them (multi-line tuple literals
|
||||
// would otherwise leave line-introducing NOPs behind).
|
||||
let folded_loc = block.instructions[i].location;
|
||||
for j in start_idx..i {
|
||||
block.instructions[j].instr = Instruction::Nop.into();
|
||||
block.instructions[j].location = folded_loc;
|
||||
}
|
||||
|
||||
// Replace BUILD_TUPLE with LOAD_CONST
|
||||
@@ -673,12 +696,23 @@ impl CodeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove NOP instructions from all blocks
|
||||
/// Remove NOP instructions from all blocks, but keep NOPs that introduce
|
||||
/// a new source line (they serve as line markers for monitoring LINE events).
|
||||
fn remove_nops(&mut self) {
|
||||
for block in &mut self.blocks {
|
||||
block
|
||||
.instructions
|
||||
.retain(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)));
|
||||
let mut prev_line = None;
|
||||
block.instructions.retain(|ins| {
|
||||
if matches!(ins.instr.real(), Some(Instruction::Nop)) {
|
||||
let line = ins.location.line;
|
||||
if prev_line == Some(line) {
|
||||
// Same line as previous instruction — safe to remove
|
||||
return false;
|
||||
}
|
||||
// This NOP introduces a new line — keep it
|
||||
}
|
||||
prev_line = Some(ins.location.line);
|
||||
true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+271
-19
@@ -369,6 +369,7 @@ impl Py<Frame> {
|
||||
lasti: &self.lasti,
|
||||
object: self,
|
||||
state: &mut state,
|
||||
monitoring_mask: 0,
|
||||
};
|
||||
f(exec)
|
||||
}
|
||||
@@ -414,6 +415,7 @@ impl Py<Frame> {
|
||||
lasti: &self.lasti,
|
||||
object: self,
|
||||
state: &mut state,
|
||||
monitoring_mask: 0,
|
||||
};
|
||||
exec.yield_from_target().map(PyObject::to_owned)
|
||||
}
|
||||
@@ -448,6 +450,8 @@ struct ExecutingFrame<'a> {
|
||||
object: &'a Py<Frame>,
|
||||
lasti: &'a Lasti,
|
||||
state: &'a mut FrameState,
|
||||
/// Cached monitoring events mask. Reloaded at Resume instruction only,
|
||||
monitoring_mask: u32,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ExecutingFrame<'_> {
|
||||
@@ -498,6 +502,7 @@ impl ExecutingFrame<'_> {
|
||||
let instructions = &self.code.instructions;
|
||||
let mut arg_state = bytecode::OpArgState::default();
|
||||
let mut prev_line: u32 = 0;
|
||||
self.monitoring_mask = vm.state.monitoring_events.load();
|
||||
loop {
|
||||
let idx = self.lasti() as usize;
|
||||
// Fire 'line' trace event when line number changes.
|
||||
@@ -515,6 +520,25 @@ impl ExecutingFrame<'_> {
|
||||
let bytecode::CodeUnit { op, arg } = instructions[idx];
|
||||
let arg = arg_state.extend(arg);
|
||||
let mut do_extend_arg = false;
|
||||
|
||||
// Always track current line for LINE monitoring, even when
|
||||
// monitoring is off. This prevents spurious LINE events when
|
||||
// monitoring is enabled mid-function.
|
||||
if !matches!(op, Instruction::Resume { .. } | Instruction::ExtendedArg) {
|
||||
let line = self.code.locations[idx].0.line.get() as u32;
|
||||
if self.monitoring_mask != 0 {
|
||||
use crate::stdlib::sys::monitoring;
|
||||
let offset = idx as u32 * 2;
|
||||
if self.monitoring_mask & monitoring::EVENT_LINE != 0 && line != prev_line {
|
||||
monitoring::fire_line(vm, self.code, offset, line)?;
|
||||
}
|
||||
if self.monitoring_mask & monitoring::EVENT_INSTRUCTION != 0 {
|
||||
monitoring::fire_instruction(vm, self.code, offset)?;
|
||||
}
|
||||
}
|
||||
prev_line = line;
|
||||
}
|
||||
|
||||
let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm);
|
||||
match result {
|
||||
Ok(None) => {}
|
||||
@@ -601,10 +625,57 @@ impl ExecutingFrame<'_> {
|
||||
)
|
||||
);
|
||||
|
||||
// Fire RAISE or RERAISE monitoring event.
|
||||
// fire_reraise internally deduplicates: only the first
|
||||
// re-raise after each EXCEPTION_HANDLED fires the event.
|
||||
// If the callback raises (e.g. ValueError for illegal DISABLE),
|
||||
// replace the original exception.
|
||||
let exception = {
|
||||
use crate::stdlib::sys::monitoring;
|
||||
if is_reraise {
|
||||
if self.monitoring_mask & monitoring::EVENT_RERAISE != 0 {
|
||||
let offset = idx as u32 * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
match monitoring::fire_reraise(vm, self.code, offset, &exc_obj) {
|
||||
Ok(()) => exception,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exception
|
||||
}
|
||||
} else if self.monitoring_mask & monitoring::EVENT_RAISE != 0 {
|
||||
let offset = idx as u32 * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
match monitoring::fire_raise(vm, self.code, offset, &exc_obj) {
|
||||
Ok(()) => exception,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exception
|
||||
}
|
||||
};
|
||||
|
||||
match handle_exception(self, exception, idx, is_reraise, is_new_raise, vm) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(result)) => break Ok(result),
|
||||
Err(exception) => {
|
||||
// Fire PY_UNWIND: exception escapes this frame
|
||||
let exception = if self.monitoring_mask
|
||||
& crate::stdlib::sys::monitoring::EVENT_PY_UNWIND
|
||||
!= 0
|
||||
{
|
||||
let offset = idx as u32 * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
match crate::stdlib::sys::monitoring::fire_py_unwind(
|
||||
vm, self.code, offset, &exc_obj,
|
||||
) {
|
||||
Ok(()) => exception,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exception
|
||||
};
|
||||
|
||||
// Restore lasti from traceback so frame.f_lineno matches tb_lineno
|
||||
// The traceback was created with the correct lasti when exception
|
||||
// was first raised, but frame.lasti may have changed during cleanup
|
||||
@@ -671,6 +742,7 @@ impl ExecutingFrame<'_> {
|
||||
exc_val: PyObjectRef,
|
||||
exc_tb: PyObjectRef,
|
||||
) -> PyResult<ExecutionResult> {
|
||||
self.monitoring_mask = vm.state.monitoring_events.load();
|
||||
if let Some(jen) = self.yield_from_target() {
|
||||
// Check if the exception is GeneratorExit (type or instance).
|
||||
// For GeneratorExit, close the sub-iterator instead of throwing.
|
||||
@@ -781,6 +853,33 @@ impl ExecutingFrame<'_> {
|
||||
exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
|
||||
}
|
||||
|
||||
// Fire PY_THROW and RAISE events before raising the exception in the
|
||||
// generator. do_monitor_exc in CPython replaces the active exception
|
||||
// when a callback fails, so we mirror that here.
|
||||
let exception = {
|
||||
use crate::stdlib::sys::monitoring;
|
||||
let exception = if self.monitoring_mask & monitoring::EVENT_PY_THROW != 0 {
|
||||
let offset = idx as u32 * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
match monitoring::fire_py_throw(vm, self.code, offset, &exc_obj) {
|
||||
Ok(()) => exception,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exception
|
||||
};
|
||||
if self.monitoring_mask & monitoring::EVENT_RAISE != 0 {
|
||||
let offset = idx as u32 * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
match monitoring::fire_raise(vm, self.code, offset, &exc_obj) {
|
||||
Ok(()) => exception,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exception
|
||||
}
|
||||
};
|
||||
|
||||
// when raising an exception, set __context__ to the current exception
|
||||
// This is done in _PyErr_SetObject
|
||||
vm.contextualize_exception(&exception);
|
||||
@@ -792,7 +891,26 @@ impl ExecutingFrame<'_> {
|
||||
match self.unwind_blocks(vm, UnwindReason::Raising { exception }) {
|
||||
Ok(None) => self.run(vm),
|
||||
Ok(Some(result)) => Ok(result),
|
||||
Err(exception) => Err(exception),
|
||||
Err(exception) => {
|
||||
// Fire PY_UNWIND: exception escapes the generator frame.
|
||||
// do_monitor_exc replaces the exception on callback failure.
|
||||
let exception = if self.monitoring_mask
|
||||
& crate::stdlib::sys::monitoring::EVENT_PY_UNWIND
|
||||
!= 0
|
||||
{
|
||||
let offset = idx as u32 * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
match crate::stdlib::sys::monitoring::fire_py_unwind(
|
||||
vm, self.code, offset, &exc_obj,
|
||||
) {
|
||||
Ok(()) => exception,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exception
|
||||
};
|
||||
Err(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1371,7 +1489,18 @@ impl ExecutingFrame<'_> {
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::JumpBackward { target } => {
|
||||
self.jump(target.get(arg));
|
||||
let src_offset = (self.lasti() - 1) * 2;
|
||||
let dest = target.get(arg);
|
||||
self.jump(dest);
|
||||
// JUMP events fire only for backward jumps (loop iterations)
|
||||
if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_JUMP != 0 {
|
||||
crate::stdlib::sys::monitoring::fire_jump(
|
||||
vm,
|
||||
self.code,
|
||||
src_offset,
|
||||
dest.0 * 2,
|
||||
)?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::JumpBackwardNoInterrupt { target } => {
|
||||
@@ -1976,16 +2105,24 @@ impl ExecutingFrame<'_> {
|
||||
Instruction::PopJumpIfTrue { target } => self.pop_jump_if(vm, target.get(arg), true),
|
||||
Instruction::PopJumpIfNone { target } => {
|
||||
let value = self.pop_value();
|
||||
if vm.is_none(&value) {
|
||||
self.jump(target.get(arg));
|
||||
let branch_taken = vm.is_none(&value);
|
||||
let target = target.get(arg);
|
||||
let src_offset = (self.lasti() - 1) * 2;
|
||||
if branch_taken {
|
||||
self.jump(target);
|
||||
}
|
||||
self.fire_branch_event(vm, src_offset, branch_taken, target)?;
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::PopJumpIfNotNone { target } => {
|
||||
let value = self.pop_value();
|
||||
if !vm.is_none(&value) {
|
||||
self.jump(target.get(arg));
|
||||
let branch_taken = !vm.is_none(&value);
|
||||
let target = target.get(arg);
|
||||
let src_offset = (self.lasti() - 1) * 2;
|
||||
if branch_taken {
|
||||
self.jump(target);
|
||||
}
|
||||
self.fire_branch_event(vm, src_offset, branch_taken, target)?;
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::PopTop => {
|
||||
@@ -2010,19 +2147,25 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::RaiseVarargs { kind } => self.execute_raise(vm, kind.get(arg)),
|
||||
Instruction::Resume { arg: resume_arg } => {
|
||||
// Resume execution after yield, await, or at function start
|
||||
// In CPython, this checks instrumentation and eval breaker
|
||||
// For now, we just check for signals/interrupts
|
||||
let _resume_type = resume_arg.get(arg);
|
||||
|
||||
// Check for interrupts if not resuming from yield_from
|
||||
// if resume_type < bytecode::ResumeType::AfterYieldFrom as u32 {
|
||||
// vm.check_signals()?;
|
||||
// }
|
||||
use crate::stdlib::sys::monitoring;
|
||||
let resume_type = resume_arg.get(arg);
|
||||
self.monitoring_mask = vm.state.monitoring_events.load();
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
if resume_type == 0 {
|
||||
if self.monitoring_mask & monitoring::EVENT_PY_START != 0 {
|
||||
monitoring::fire_py_start(vm, self.code, offset)?;
|
||||
}
|
||||
} else if self.monitoring_mask & monitoring::EVENT_PY_RESUME != 0 {
|
||||
monitoring::fire_py_resume(vm, self.code, offset)?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::ReturnValue => {
|
||||
let value = self.pop_value();
|
||||
if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_PY_RETURN != 0 {
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
crate::stdlib::sys::monitoring::fire_py_return(vm, self.code, offset, &value)?;
|
||||
}
|
||||
self.unwind_blocks(vm, UnwindReason::Returning { value })
|
||||
}
|
||||
Instruction::SetAdd { i } => {
|
||||
@@ -2061,8 +2204,10 @@ impl ExecutingFrame<'_> {
|
||||
vm.set_exception(Some(exc_ref.to_owned()));
|
||||
}
|
||||
|
||||
// Complete stack operations
|
||||
self.push_value(prev_exc);
|
||||
self.push_value(exc);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::CheckExcMatch => {
|
||||
@@ -2243,6 +2388,10 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::YieldValue { arg: oparg } => {
|
||||
let value = self.pop_value();
|
||||
if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_PY_YIELD != 0 {
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
crate::stdlib::sys::monitoring::fire_py_yield(vm, self.code, offset, &value)?;
|
||||
}
|
||||
// arg=0: direct yield (wrapped for async generators)
|
||||
// arg=1: yield from await/yield-from (NOT wrapped)
|
||||
let wrap = oparg.get(arg) == 0;
|
||||
@@ -2571,6 +2720,23 @@ impl ExecutingFrame<'_> {
|
||||
if let Some(entry) =
|
||||
bytecode::find_exception_handler(&self.code.exceptiontable, offset)
|
||||
{
|
||||
// Fire EXCEPTION_HANDLED before setting up handler.
|
||||
// If the callback raises, the handler is NOT set up and the
|
||||
// new exception propagates instead.
|
||||
if self.monitoring_mask
|
||||
& crate::stdlib::sys::monitoring::EVENT_EXCEPTION_HANDLED
|
||||
!= 0
|
||||
{
|
||||
let byte_offset = offset * 2;
|
||||
let exc_obj: PyObjectRef = exception.clone().into();
|
||||
crate::stdlib::sys::monitoring::fire_exception_handled(
|
||||
vm,
|
||||
self.code,
|
||||
byte_offset,
|
||||
&exc_obj,
|
||||
)?;
|
||||
}
|
||||
|
||||
// 1. Pop stack to entry.depth
|
||||
while self.state.stack.len() > entry.depth as usize {
|
||||
self.state.stack.pop();
|
||||
@@ -2774,6 +2940,8 @@ impl ExecutingFrame<'_> {
|
||||
|
||||
#[inline]
|
||||
fn execute_call(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult {
|
||||
use crate::stdlib::sys::monitoring;
|
||||
|
||||
// Stack: [callable, self_or_null, ...]
|
||||
let self_or_null = self.pop_value_opt(); // Option<PyObjectRef>
|
||||
let callable = self.pop_value();
|
||||
@@ -2792,9 +2960,50 @@ impl ExecutingFrame<'_> {
|
||||
args
|
||||
};
|
||||
|
||||
let value = callable.call(final_args, vm)?;
|
||||
self.push_value(value);
|
||||
Ok(None)
|
||||
let is_python_call = callable.downcast_ref::<PyFunction>().is_some();
|
||||
|
||||
// Compute arg0 once for CALL, C_RETURN, and C_RAISE events
|
||||
let call_arg0 = if self.monitoring_mask & monitoring::EVENT_CALL != 0 {
|
||||
let arg0 = final_args
|
||||
.args
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| monitoring::get_missing(vm));
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
monitoring::fire_call(vm, self.code, offset, &callable, arg0.clone())?;
|
||||
Some(arg0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match callable.call(final_args, vm) {
|
||||
Ok(value) => {
|
||||
if let Some(arg0) = call_arg0
|
||||
&& !is_python_call
|
||||
{
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
monitoring::fire_c_return(vm, self.code, offset, &callable, arg0)?;
|
||||
}
|
||||
self.push_value(value);
|
||||
Ok(None)
|
||||
}
|
||||
Err(exc) => {
|
||||
// call_instrumentation_vector_protected replaces the active
|
||||
// exception when the callback itself raises.
|
||||
let exc = if let Some(arg0) = call_arg0
|
||||
&& !is_python_call
|
||||
{
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
match monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0) {
|
||||
Ok(()) => exc,
|
||||
Err(monitor_exc) => monitor_exc,
|
||||
}
|
||||
} else {
|
||||
exc
|
||||
};
|
||||
Err(exc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_raise(&mut self, vm: &VirtualMachine, kind: bytecode::RaiseKind) -> FrameResult {
|
||||
@@ -2923,6 +3132,43 @@ impl ExecutingFrame<'_> {
|
||||
self.update_lasti(|i| *i = target_pc);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn fire_branch_event(
|
||||
&self,
|
||||
vm: &VirtualMachine,
|
||||
src_offset: u32,
|
||||
branch_taken: bool,
|
||||
target: bytecode::Label,
|
||||
) -> PyResult<()> {
|
||||
if self.monitoring_mask
|
||||
& (crate::stdlib::sys::monitoring::EVENT_BRANCH_LEFT
|
||||
| crate::stdlib::sys::monitoring::EVENT_BRANCH_RIGHT)
|
||||
!= 0
|
||||
{
|
||||
let dest_offset = if branch_taken {
|
||||
target.0 * 2
|
||||
} else {
|
||||
self.lasti() * 2
|
||||
};
|
||||
if branch_taken {
|
||||
crate::stdlib::sys::monitoring::fire_branch_right(
|
||||
vm,
|
||||
self.code,
|
||||
src_offset,
|
||||
dest_offset,
|
||||
)?;
|
||||
} else {
|
||||
crate::stdlib::sys::monitoring::fire_branch_left(
|
||||
vm,
|
||||
self.code,
|
||||
src_offset,
|
||||
dest_offset,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn pop_jump_if(
|
||||
&mut self,
|
||||
@@ -2932,9 +3178,12 @@ impl ExecutingFrame<'_> {
|
||||
) -> FrameResult {
|
||||
let obj = self.pop_value();
|
||||
let value = obj.try_to_bool(vm)?;
|
||||
if value == flag {
|
||||
let branch_taken = value == flag;
|
||||
let src_offset = (self.lasti() - 1) * 2;
|
||||
if branch_taken {
|
||||
self.jump(target);
|
||||
}
|
||||
self.fire_branch_event(vm, src_offset, branch_taken, target)?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -2942,11 +3191,13 @@ impl ExecutingFrame<'_> {
|
||||
fn execute_for_iter(&mut self, vm: &VirtualMachine, target: bytecode::Label) -> FrameResult {
|
||||
let top_of_stack = PyIter::new(self.top_value());
|
||||
let next_obj = top_of_stack.next(vm);
|
||||
let src_offset = (self.lasti() - 1) * 2;
|
||||
|
||||
// Check the next object:
|
||||
match next_obj {
|
||||
Ok(PyIterReturn::Return(value)) => {
|
||||
self.push_value(value);
|
||||
self.fire_branch_event(vm, src_offset, false, target)?;
|
||||
Ok(None)
|
||||
}
|
||||
Ok(PyIterReturn::StopIteration(_)) => {
|
||||
@@ -2970,6 +3221,7 @@ impl ExecutingFrame<'_> {
|
||||
target
|
||||
};
|
||||
self.jump(jump_target);
|
||||
self.fire_branch_event(vm, src_offset, true, jump_target)?;
|
||||
Ok(None)
|
||||
}
|
||||
Err(next_error) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub(crate) mod monitoring;
|
||||
|
||||
use crate::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule, convert::ToPyObject};
|
||||
|
||||
#[cfg(all(not(feature = "host_env"), feature = "stdio"))]
|
||||
@@ -73,6 +75,9 @@ mod sys {
|
||||
RUST_MULTIARCH.replace("-unknown", "")
|
||||
}
|
||||
|
||||
#[pymodule(name = "monitoring", with(super::monitoring::sys_monitoring))]
|
||||
pub(super) mod monitoring {}
|
||||
|
||||
#[pyclass(no_attr, name = "_BootstrapStderr")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
pub(super) struct BootstrapStderr;
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
use crate::{
|
||||
AsObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||
builtins::{PyCode, PyDictRef, PyNamespace, PyUtf8StrRef},
|
||||
function::FuncArgs,
|
||||
};
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub const TOOL_LIMIT: usize = 6;
|
||||
const EVENTS_COUNT: usize = 19;
|
||||
const LOCAL_EVENTS_COUNT: usize = 11;
|
||||
const UNGROUPED_EVENTS_COUNT: usize = 18;
|
||||
|
||||
// Event bit positions
|
||||
bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MonitoringEvents: u32 {
|
||||
const PY_START = 1 << 0;
|
||||
const PY_RESUME = 1 << 1;
|
||||
const PY_RETURN = 1 << 2;
|
||||
const PY_YIELD = 1 << 3;
|
||||
const CALL = 1 << 4;
|
||||
const LINE = 1 << 5;
|
||||
const INSTRUCTION = 1 << 6;
|
||||
const JUMP = 1 << 7;
|
||||
const BRANCH_LEFT = 1 << 8;
|
||||
const BRANCH_RIGHT = 1 << 9;
|
||||
const STOP_ITERATION = 1 << 10;
|
||||
const RAISE = 1 << 11;
|
||||
const EXCEPTION_HANDLED = 1 << 12;
|
||||
const PY_UNWIND = 1 << 13;
|
||||
const PY_THROW = 1 << 14;
|
||||
const RERAISE = 1 << 15;
|
||||
const C_RETURN = 1 << 16;
|
||||
const C_RAISE = 1 << 17;
|
||||
const BRANCH = 1 << 18;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export as plain u32 constants for use in frame.rs hot-path checks
|
||||
pub const EVENT_PY_START: u32 = MonitoringEvents::PY_START.bits();
|
||||
pub const EVENT_PY_RESUME: u32 = MonitoringEvents::PY_RESUME.bits();
|
||||
pub const EVENT_PY_RETURN: u32 = MonitoringEvents::PY_RETURN.bits();
|
||||
pub const EVENT_PY_YIELD: u32 = MonitoringEvents::PY_YIELD.bits();
|
||||
pub const EVENT_CALL: u32 = MonitoringEvents::CALL.bits();
|
||||
pub const EVENT_LINE: u32 = MonitoringEvents::LINE.bits();
|
||||
pub const EVENT_INSTRUCTION: u32 = MonitoringEvents::INSTRUCTION.bits();
|
||||
pub const EVENT_JUMP: u32 = MonitoringEvents::JUMP.bits();
|
||||
pub const EVENT_BRANCH_LEFT: u32 = MonitoringEvents::BRANCH_LEFT.bits();
|
||||
pub const EVENT_BRANCH_RIGHT: u32 = MonitoringEvents::BRANCH_RIGHT.bits();
|
||||
pub const EVENT_RAISE: u32 = MonitoringEvents::RAISE.bits();
|
||||
pub const EVENT_EXCEPTION_HANDLED: u32 = MonitoringEvents::EXCEPTION_HANDLED.bits();
|
||||
pub const EVENT_PY_UNWIND: u32 = MonitoringEvents::PY_UNWIND.bits();
|
||||
pub const EVENT_C_RETURN: u32 = MonitoringEvents::C_RETURN.bits();
|
||||
const EVENT_C_RAISE: u32 = MonitoringEvents::C_RAISE.bits();
|
||||
const EVENT_STOP_ITERATION: u32 = MonitoringEvents::STOP_ITERATION.bits();
|
||||
pub const EVENT_PY_THROW: u32 = MonitoringEvents::PY_THROW.bits();
|
||||
const EVENT_BRANCH: u32 = MonitoringEvents::BRANCH.bits();
|
||||
pub const EVENT_RERAISE: u32 = MonitoringEvents::RERAISE.bits();
|
||||
const EVENT_C_RETURN_MASK: u32 = EVENT_C_RETURN | EVENT_C_RAISE;
|
||||
|
||||
const EVENT_NAMES: [&str; EVENTS_COUNT] = [
|
||||
"PY_START",
|
||||
"PY_RESUME",
|
||||
"PY_RETURN",
|
||||
"PY_YIELD",
|
||||
"CALL",
|
||||
"LINE",
|
||||
"INSTRUCTION",
|
||||
"JUMP",
|
||||
"BRANCH_LEFT",
|
||||
"BRANCH_RIGHT",
|
||||
"STOP_ITERATION",
|
||||
"RAISE",
|
||||
"EXCEPTION_HANDLED",
|
||||
"PY_UNWIND",
|
||||
"PY_THROW",
|
||||
"RERAISE",
|
||||
"C_RETURN",
|
||||
"C_RAISE",
|
||||
"BRANCH",
|
||||
];
|
||||
|
||||
/// Interpreter-level monitoring state, shared by all threads.
|
||||
pub struct MonitoringState {
|
||||
pub tool_names: [Option<String>; TOOL_LIMIT],
|
||||
pub global_events: [u32; TOOL_LIMIT],
|
||||
pub local_events: HashMap<(usize, usize), u32>,
|
||||
pub callbacks: HashMap<(usize, usize), PyObjectRef>,
|
||||
/// Per-instruction disabled tools: (code_id, offset, tool)
|
||||
pub disabled: HashSet<(usize, usize, usize)>,
|
||||
/// Cached MISSING sentinel singleton
|
||||
pub missing: Option<PyObjectRef>,
|
||||
/// Cached DISABLE sentinel singleton
|
||||
pub disable: Option<PyObjectRef>,
|
||||
}
|
||||
|
||||
impl Default for MonitoringState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tool_names: Default::default(),
|
||||
global_events: [0; TOOL_LIMIT],
|
||||
local_events: HashMap::new(),
|
||||
callbacks: HashMap::new(),
|
||||
disabled: HashSet::new(),
|
||||
missing: None,
|
||||
disable: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitoringState {
|
||||
/// Compute the OR of all tools' global_events + local_events.
|
||||
/// This is used for the fast-path atomic mask to skip monitoring
|
||||
/// when no events are registered at all.
|
||||
pub fn combined_events(&self) -> u32 {
|
||||
let global = self.global_events.iter().fold(0, |acc, &e| acc | e);
|
||||
let local = self.local_events.values().fold(0, |acc, &e| acc | e);
|
||||
global | local
|
||||
}
|
||||
}
|
||||
|
||||
/// Global atomic mask: OR of all tools' events. Checked in the hot path
|
||||
/// to skip monitoring overhead when no events are registered.
|
||||
/// Lives in PyGlobalState alongside the PyMutex<MonitoringState>.
|
||||
pub type MonitoringEventsMask = AtomicCell<u32>;
|
||||
|
||||
/// Get the MISSING sentinel, creating it if necessary.
|
||||
pub fn get_missing(vm: &VirtualMachine) -> PyObjectRef {
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
if let Some(ref m) = state.missing {
|
||||
m.clone()
|
||||
} else {
|
||||
let m: PyObjectRef = sys_monitoring::MonitoringSentinel.into_ref(&vm.ctx).into();
|
||||
state.missing = Some(m.clone());
|
||||
m
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the DISABLE sentinel, creating it if necessary.
|
||||
pub fn get_disable(vm: &VirtualMachine) -> PyObjectRef {
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
if let Some(ref d) = state.disable {
|
||||
d.clone()
|
||||
} else {
|
||||
let d: PyObjectRef = sys_monitoring::MonitoringSentinel.into_ref(&vm.ctx).into();
|
||||
state.disable = Some(d.clone());
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
fn check_valid_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult<usize> {
|
||||
if !(0..TOOL_LIMIT as i32).contains(&tool_id) {
|
||||
return Err(vm.new_value_error(format!("invalid tool {tool_id} (must be between 0 and 5)")));
|
||||
}
|
||||
Ok(tool_id as usize)
|
||||
}
|
||||
|
||||
fn check_tool_in_use(tool: usize, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let state = vm.state.monitoring.lock();
|
||||
if state.tool_names[tool].is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(vm.new_value_error(format!("tool {tool} is not in use")))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_single_event(event: i32, vm: &VirtualMachine) -> PyResult<usize> {
|
||||
let event = u32::try_from(event)
|
||||
.map_err(|_| vm.new_value_error("The callback can only be set for one event at a time"))?;
|
||||
if event.count_ones() != 1 {
|
||||
return Err(vm.new_value_error("The callback can only be set for one event at a time"));
|
||||
}
|
||||
let event_id = event.trailing_zeros() as usize;
|
||||
if event_id >= EVENTS_COUNT {
|
||||
return Err(vm.new_value_error(format!("invalid event {event}")));
|
||||
}
|
||||
Ok(event_id)
|
||||
}
|
||||
|
||||
fn normalize_event_set(event_set: i32, local: bool, vm: &VirtualMachine) -> PyResult<u32> {
|
||||
if event_set < 0 {
|
||||
let kind = if local {
|
||||
"local event set"
|
||||
} else {
|
||||
"event set"
|
||||
};
|
||||
return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}")));
|
||||
}
|
||||
|
||||
let mut event_set = event_set as u32;
|
||||
if event_set >= (1 << EVENTS_COUNT) {
|
||||
let kind = if local {
|
||||
"local event set"
|
||||
} else {
|
||||
"event set"
|
||||
};
|
||||
return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}")));
|
||||
}
|
||||
|
||||
if (event_set & EVENT_C_RETURN_MASK) != 0 && (event_set & EVENT_CALL) != EVENT_CALL {
|
||||
return Err(vm.new_value_error("cannot set C_RETURN or C_RAISE events independently"));
|
||||
}
|
||||
|
||||
event_set &= !EVENT_C_RETURN_MASK;
|
||||
|
||||
if (event_set & EVENT_BRANCH) != 0 {
|
||||
event_set &= !EVENT_BRANCH;
|
||||
event_set |= EVENT_BRANCH_LEFT | EVENT_BRANCH_RIGHT;
|
||||
}
|
||||
|
||||
if local && event_set >= (1 << LOCAL_EVENTS_COUNT) {
|
||||
return Err(vm.new_value_error(format!("invalid local event set 0x{event_set:x}")));
|
||||
}
|
||||
|
||||
Ok(event_set)
|
||||
}
|
||||
|
||||
/// Update the global monitoring_events atomic mask from current state.
|
||||
fn update_events_mask(vm: &VirtualMachine, state: &MonitoringState) {
|
||||
vm.state.monitoring_events.store(state.combined_events());
|
||||
}
|
||||
|
||||
fn use_tool_id(tool_id: i32, name: &str, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
if state.tool_names[tool].is_some() {
|
||||
return Err(vm.new_value_error(format!("tool {tool_id} is already in use")));
|
||||
}
|
||||
state.tool_names[tool] = Some(name.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
if state.tool_names[tool].is_some() {
|
||||
state.global_events[tool] = 0;
|
||||
state
|
||||
.local_events
|
||||
.retain(|(local_tool, _), _| *local_tool != tool);
|
||||
state.callbacks.retain(|(cb_tool, _), _| *cb_tool != tool);
|
||||
state.disabled.retain(|&(_, _, t)| t != tool);
|
||||
}
|
||||
update_events_mask(vm, &state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
if state.tool_names[tool].is_some() {
|
||||
state.global_events[tool] = 0;
|
||||
state
|
||||
.local_events
|
||||
.retain(|(local_tool, _), _| *local_tool != tool);
|
||||
state.callbacks.retain(|(cb_tool, _), _| *cb_tool != tool);
|
||||
state.disabled.retain(|&(_, _, t)| t != tool);
|
||||
state.tool_names[tool] = None;
|
||||
}
|
||||
update_events_mask(vm, &state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult<Option<String>> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let state = vm.state.monitoring.lock();
|
||||
Ok(state.tool_names[tool].clone())
|
||||
}
|
||||
|
||||
fn register_callback(
|
||||
tool_id: i32,
|
||||
event: i32,
|
||||
func: PyObjectRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<PyObjectRef> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let event_id = parse_single_event(event, vm)?;
|
||||
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
let prev = state
|
||||
.callbacks
|
||||
.remove(&(tool, event_id))
|
||||
.unwrap_or_else(|| vm.ctx.none());
|
||||
let branch_id = EVENT_BRANCH.trailing_zeros() as usize;
|
||||
let branch_left_id = EVENT_BRANCH_LEFT.trailing_zeros() as usize;
|
||||
let branch_right_id = EVENT_BRANCH_RIGHT.trailing_zeros() as usize;
|
||||
if !vm.is_none(&func) {
|
||||
state.callbacks.insert((tool, event_id), func.clone());
|
||||
// BRANCH is a composite event: also register for BRANCH_LEFT/RIGHT
|
||||
if event_id == branch_id {
|
||||
state.callbacks.insert((tool, branch_left_id), func.clone());
|
||||
state.callbacks.insert((tool, branch_right_id), func);
|
||||
}
|
||||
} else {
|
||||
// Also clear BRANCH_LEFT/RIGHT when clearing BRANCH
|
||||
if event_id == branch_id {
|
||||
state.callbacks.remove(&(tool, branch_left_id));
|
||||
state.callbacks.remove(&(tool, branch_right_id));
|
||||
}
|
||||
}
|
||||
Ok(prev)
|
||||
}
|
||||
|
||||
fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult<u32> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let state = vm.state.monitoring.lock();
|
||||
Ok(state.global_events[tool])
|
||||
}
|
||||
|
||||
fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
check_tool_in_use(tool, vm)?;
|
||||
let normalized = normalize_event_set(event_set, false, vm)?;
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
state.global_events[tool] = normalized;
|
||||
update_events_mask(vm, &state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_local_events(tool_id: i32, code: PyObjectRef, vm: &VirtualMachine) -> PyResult<u32> {
|
||||
if code.downcast_ref::<PyCode>().is_none() {
|
||||
return Err(vm.new_type_error("code must be a code object"));
|
||||
}
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
let code_id = code.get_id();
|
||||
let state = vm.state.monitoring.lock();
|
||||
Ok(state
|
||||
.local_events
|
||||
.get(&(tool, code_id))
|
||||
.copied()
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
fn set_local_events(
|
||||
tool_id: i32,
|
||||
code: PyObjectRef,
|
||||
event_set: i32,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
if code.downcast_ref::<PyCode>().is_none() {
|
||||
return Err(vm.new_type_error("code must be a code object"));
|
||||
}
|
||||
let tool = check_valid_tool(tool_id, vm)?;
|
||||
check_tool_in_use(tool, vm)?;
|
||||
let normalized = normalize_event_set(event_set, true, vm)?;
|
||||
let code_id = code.get_id();
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
if normalized == 0 {
|
||||
state.local_events.remove(&(tool, code_id));
|
||||
} else {
|
||||
state.local_events.insert((tool, code_id), normalized);
|
||||
}
|
||||
update_events_mask(vm, &state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restart_events(vm: &VirtualMachine) {
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
state.disabled.clear();
|
||||
}
|
||||
|
||||
fn all_events(vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
||||
// Collect data under the lock, then release before calling into Python VM.
|
||||
let masks: Vec<(&str, u8)> = {
|
||||
let state = vm.state.monitoring.lock();
|
||||
EVENT_NAMES
|
||||
.iter()
|
||||
.take(UNGROUPED_EVENTS_COUNT)
|
||||
.enumerate()
|
||||
.filter_map(|(event_id, event_name)| {
|
||||
let event_bit = 1u32 << event_id;
|
||||
let mut tools_mask = 0u8;
|
||||
for tool in 0..TOOL_LIMIT {
|
||||
if (state.global_events[tool] & event_bit) != 0 {
|
||||
tools_mask |= 1 << tool;
|
||||
}
|
||||
}
|
||||
if tools_mask != 0 {
|
||||
Some((*event_name, tools_mask))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let all_events = vm.ctx.new_dict();
|
||||
for (name, mask) in masks {
|
||||
all_events.set_item(name, vm.ctx.new_int(mask).into(), vm)?;
|
||||
}
|
||||
Ok(all_events)
|
||||
}
|
||||
|
||||
// Event dispatch
|
||||
|
||||
use core::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
/// Re-entrancy guard: prevents monitoring callbacks from triggering
|
||||
/// additional monitoring events (which would cause infinite recursion).
|
||||
static FIRING: Cell<bool> = const { Cell::new(false) };
|
||||
|
||||
/// Tracks whether a RERAISE event has been fired since the last
|
||||
/// EXCEPTION_HANDLED. Used to suppress duplicate RERAISE from
|
||||
/// cleanup handlers that chain through multiple exception table entries.
|
||||
static RERAISE_PENDING: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Fire an event for all tools that have the event bit set.
|
||||
/// `cb_extra` contains the callback arguments after the code object.
|
||||
fn fire(
|
||||
vm: &VirtualMachine,
|
||||
event: u32,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
cb_extra: &[PyObjectRef],
|
||||
) -> PyResult<()> {
|
||||
// Prevent recursive event firing
|
||||
if FIRING.with(|f| f.get()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let event_id = event.trailing_zeros() as usize;
|
||||
let code_id = code.get_id();
|
||||
|
||||
// C_RETURN and C_RAISE are implicitly enabled when CALL is set.
|
||||
let check_bit = if event & EVENT_C_RETURN_MASK != 0 {
|
||||
event | EVENT_CALL
|
||||
} else {
|
||||
event
|
||||
};
|
||||
|
||||
// Collect callbacks and snapshot the DISABLE sentinel under a single lock.
|
||||
let (callbacks, disable_sentinel): (Vec<(usize, PyObjectRef)>, Option<PyObjectRef>) = {
|
||||
let state = vm.state.monitoring.lock();
|
||||
let mut cbs = Vec::new();
|
||||
for tool in 0..TOOL_LIMIT {
|
||||
let global = state.global_events[tool];
|
||||
let local = state
|
||||
.local_events
|
||||
.get(&(tool, code_id))
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
if ((global | local) & check_bit) == 0 {
|
||||
continue;
|
||||
}
|
||||
if state.disabled.contains(&(code_id, offset as usize, tool)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(cb) = state.callbacks.get(&(tool, event_id)) {
|
||||
cbs.push((tool, cb.clone()));
|
||||
}
|
||||
}
|
||||
(cbs, state.disable.clone())
|
||||
};
|
||||
|
||||
if callbacks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut args_vec = Vec::with_capacity(1 + cb_extra.len());
|
||||
args_vec.push(code.clone().into());
|
||||
args_vec.extend_from_slice(cb_extra);
|
||||
let args = FuncArgs::from(args_vec);
|
||||
|
||||
FIRING.with(|f| f.set(true));
|
||||
let result = (|| {
|
||||
for (tool, cb) in callbacks {
|
||||
let result = cb.call(args.clone(), vm)?;
|
||||
if disable_sentinel.as_ref().is_some_and(|d| result.is(d)) {
|
||||
// Only local events (event_id < LOCAL_EVENTS_COUNT) can be disabled.
|
||||
// Non-local events (RAISE, EXCEPTION_HANDLED, PY_UNWIND, etc.)
|
||||
// cannot be disabled per code object.
|
||||
if event_id >= LOCAL_EVENTS_COUNT {
|
||||
return Err(vm.new_value_error(format!(
|
||||
"cannot disable {} events",
|
||||
EVENT_NAMES[event_id]
|
||||
)));
|
||||
}
|
||||
let mut state = vm.state.monitoring.lock();
|
||||
state.disabled.insert((code_id, offset as usize, tool));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
FIRING.with(|f| f.set(false));
|
||||
result
|
||||
}
|
||||
|
||||
// Public dispatch functions (called from frame.rs)
|
||||
|
||||
pub fn fire_py_start(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_PY_START,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_py_resume(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_PY_RESUME,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_py_return(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
retval: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_PY_RETURN,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), retval.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_py_yield(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
retval: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_PY_YIELD,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), retval.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_call(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
callable: &PyObjectRef,
|
||||
arg0: PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_CALL,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), callable.clone(), arg0],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_c_return(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
callable: &PyObjectRef,
|
||||
arg0: PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_C_RETURN,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), callable.clone(), arg0],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_c_raise(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
callable: &PyObjectRef,
|
||||
arg0: PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_C_RAISE,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), callable.clone(), arg0],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_line(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
line: u32,
|
||||
) -> PyResult<()> {
|
||||
fire(vm, EVENT_LINE, code, offset, &[vm.ctx.new_int(line).into()])
|
||||
}
|
||||
|
||||
pub fn fire_instruction(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_INSTRUCTION,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_raise(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
exception: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_RAISE,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), exception.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
/// Only fires if no RERAISE has been fired since the last EXCEPTION_HANDLED,
|
||||
/// preventing duplicate events from chained cleanup handlers.
|
||||
pub fn fire_reraise(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
exception: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
if RERAISE_PENDING.with(|f| f.get()) {
|
||||
return Ok(());
|
||||
}
|
||||
RERAISE_PENDING.with(|f| f.set(true));
|
||||
fire(
|
||||
vm,
|
||||
EVENT_RERAISE,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), exception.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_exception_handled(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
exception: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
RERAISE_PENDING.with(|f| f.set(false));
|
||||
fire(
|
||||
vm,
|
||||
EVENT_EXCEPTION_HANDLED,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), exception.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_py_unwind(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
exception: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
RERAISE_PENDING.with(|f| f.set(false));
|
||||
fire(
|
||||
vm,
|
||||
EVENT_PY_UNWIND,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), exception.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_py_throw(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
exception: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_PY_THROW,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), exception.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn fire_stop_iteration(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
exception: &PyObjectRef,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_STOP_ITERATION,
|
||||
code,
|
||||
offset,
|
||||
&[vm.ctx.new_int(offset).into(), exception.clone()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_jump(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
destination: u32,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_JUMP,
|
||||
code,
|
||||
offset,
|
||||
&[
|
||||
vm.ctx.new_int(offset).into(),
|
||||
vm.ctx.new_int(destination).into(),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_branch_left(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
destination: u32,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_BRANCH_LEFT,
|
||||
code,
|
||||
offset,
|
||||
&[
|
||||
vm.ctx.new_int(offset).into(),
|
||||
vm.ctx.new_int(destination).into(),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fire_branch_right(
|
||||
vm: &VirtualMachine,
|
||||
code: &PyRef<PyCode>,
|
||||
offset: u32,
|
||||
destination: u32,
|
||||
) -> PyResult<()> {
|
||||
fire(
|
||||
vm,
|
||||
EVENT_BRANCH_RIGHT,
|
||||
code,
|
||||
offset,
|
||||
&[
|
||||
vm.ctx.new_int(offset).into(),
|
||||
vm.ctx.new_int(destination).into(),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[pymodule(sub)]
|
||||
pub(super) mod sys_monitoring {
|
||||
use super::*;
|
||||
|
||||
#[pyclass(no_attr, module = "sys.monitoring", name = "_Sentinel")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
pub(super) struct MonitoringSentinel;
|
||||
|
||||
#[pyclass]
|
||||
impl MonitoringSentinel {}
|
||||
|
||||
#[pyattr(name = "DEBUGGER_ID")]
|
||||
const DEBUGGER_ID: u8 = 0;
|
||||
#[pyattr(name = "COVERAGE_ID")]
|
||||
const COVERAGE_ID: u8 = 1;
|
||||
#[pyattr(name = "PROFILER_ID")]
|
||||
const PROFILER_ID: u8 = 2;
|
||||
#[pyattr(name = "OPTIMIZER_ID")]
|
||||
const OPTIMIZER_ID: u8 = 5;
|
||||
|
||||
#[pyattr(once, name = "DISABLE")]
|
||||
fn disable(vm: &VirtualMachine) -> PyObjectRef {
|
||||
super::get_disable(vm)
|
||||
}
|
||||
|
||||
#[pyattr(once, name = "MISSING")]
|
||||
fn missing(vm: &VirtualMachine) -> PyObjectRef {
|
||||
super::get_missing(vm)
|
||||
}
|
||||
|
||||
#[pyattr(once)]
|
||||
fn events(vm: &VirtualMachine) -> PyRef<PyNamespace> {
|
||||
let events = PyNamespace::default().into_ref(&vm.ctx);
|
||||
for (event_id, event_name) in EVENT_NAMES.iter().enumerate() {
|
||||
events
|
||||
.as_object()
|
||||
.set_attr(*event_name, vm.ctx.new_int(1u32 << event_id), vm)
|
||||
.expect("setting sys.monitoring.events attribute should not fail");
|
||||
}
|
||||
events
|
||||
.as_object()
|
||||
.set_attr("NO_EVENTS", vm.ctx.new_int(0), vm)
|
||||
.expect("setting sys.monitoring.events.NO_EVENTS should not fail");
|
||||
events
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn use_tool_id(tool_id: i32, name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> {
|
||||
super::use_tool_id(tool_id, name.as_str(), vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn clear_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
super::clear_tool_id(tool_id, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
super::free_tool_id(tool_id, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult<Option<String>> {
|
||||
super::get_tool(tool_id, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn register_callback(
|
||||
tool_id: i32,
|
||||
event: i32,
|
||||
func: PyObjectRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<PyObjectRef> {
|
||||
super::register_callback(tool_id, event, func, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult<u32> {
|
||||
super::get_events(tool_id, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
super::set_events(tool_id, event_set, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_local_events(tool_id: i32, code: PyObjectRef, vm: &VirtualMachine) -> PyResult<u32> {
|
||||
super::get_local_events(tool_id, code, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn set_local_events(
|
||||
tool_id: i32,
|
||||
code: PyObjectRef,
|
||||
event_set: i32,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
super::set_local_events(tool_id, code, event_set, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn restart_events(vm: &VirtualMachine) {
|
||||
super::restart_events(vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _all_events(vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
||||
super::all_events(vm)
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,8 @@ where
|
||||
thread_handles: parking_lot::Mutex::new(Vec::new()),
|
||||
#[cfg(feature = "threading")]
|
||||
shutdown_handles: parking_lot::Mutex::new(Vec::new()),
|
||||
monitoring: PyMutex::default(),
|
||||
monitoring_events: AtomicCell::new(0),
|
||||
});
|
||||
|
||||
// Create VM with the global state
|
||||
|
||||
@@ -155,6 +155,10 @@ pub struct PyGlobalState {
|
||||
/// Registry for non-daemon threads that need to be joined at shutdown
|
||||
#[cfg(feature = "threading")]
|
||||
pub shutdown_handles: parking_lot::Mutex<Vec<stdlib::thread::ShutdownEntry>>,
|
||||
/// sys.monitoring state (tool names, events, callbacks)
|
||||
pub monitoring: PyMutex<stdlib::sys::monitoring::MonitoringState>,
|
||||
/// Fast-path mask: OR of all tools' events. 0 means no monitoring overhead.
|
||||
pub monitoring_events: stdlib::sys::monitoring::MonitoringEventsMask,
|
||||
}
|
||||
|
||||
pub fn process_hash_secret_seed() -> u32 {
|
||||
|
||||
Reference in new issue
Block a user