⚡ Sangsakala Proxy 🏠 Home
Skip to content
Draft
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
20 changes: 14 additions & 6 deletions crates/vm/src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,7 @@ impl VirtualMachine {
/// Display `exc` through `traceback._print_exception_bltin`, falling back
/// to the native printer writing to `file`.
pub(crate) fn display_exception_to(&self, file: &PyObject, exc: &Py<PyBaseException>) {
if let Ok(print_exception) = self
.import("traceback", 0)
.and_then(|traceback| traceback.get_attr("_print_exception_bltin", self))
&& print_exception.is_callable()
&& print_exception.call((exc.to_owned(),), self).is_ok()
{
if self.try_display_exception_to(file, exc.as_object()) {
return;
}
if self
Expand All @@ -212,6 +207,19 @@ impl VirtualMachine {
let _ = self.call_method(file, "flush", ());
}

/// Try the shared Python formatter with the caller's retained output file.
/// Reporting paths provide their own native fallback if it is unavailable.
pub(crate) fn try_display_exception_to(&self, file: &PyObject, exc: &PyObject) -> bool {
self.import("traceback", 0)
.and_then(|traceback| traceback.get_attr("_print_exception_bltin", self))
.is_ok_and(|print_exception| {
print_exception.is_callable()
&& print_exception
.call((exc.to_owned(), file.to_owned()), self)
.is_ok()
})
}

pub fn write_exception<W: Write>(
&self,
output: &mut W,
Expand Down
134 changes: 62 additions & 72 deletions crates/vm/src/stdlib/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub mod sys {
},
convert::ToPyObject,
frame::FrameObjectRef,
function::{FuncArgs, KwArgs, OptionalArg, PosArgs},
function::{FuncArgs, OptionalArg, PosArgs},
stdlib::_warnings::warn,
types::PyStructSequence,
version,
Expand Down Expand Up @@ -1314,110 +1314,100 @@ pub mod sys {
Ok(PyWindowsVersion::from_data(winver, vm))
}

fn _unraisablehook(unraisable: UnraisableHookArgsData, vm: &VirtualMachine) -> PyResult<()> {
use super::PyStderr;
/// Shared by sys.__unraisablehook__ and the fallback when a user hook fails.
/// Only the outer unraisable dispatcher discards reporting failures.
pub(crate) fn write_unraisable(
unraisable: UnraisableHookArgsData,
vm: &VirtualMachine,
) -> PyResult<()> {
let Some(file) = vm.sys_module.dict().get_item_opt("stderr", vm)? else {
return Ok(());
};
if vm.is_none(&file) {
return Ok(());
}
let write = |value: PyObjectRef| vm.call_method(&file, "write", (value,)).map(drop);
let text = |value: &str| write(vm.ctx.new_str(value).into());

let stderr = PyStderr(vm);
if !vm.is_none(&unraisable.object) {
if !vm.is_none(&unraisable.err_msg) {
write!(stderr, "{}: ", unraisable.err_msg.str(vm)?);
write(unraisable.err_msg.str(vm)?.into())?;
text(": ")?;
} else {
write!(stderr, "Exception ignored in: ");
text("Exception ignored in: ")?;
}
// exception in del will be ignored but printed
let repr = &unraisable.object.repr(vm);
let str = match repr {
Ok(v) => v.to_string(),
Err(_) => format!(
"<object {} repr() failed>",
unraisable.object.class().name()
),
};
writeln!(stderr, "{str}");
if unraisable
.object
.repr(vm)
.and_then(|value| write(value.into()))
.is_err()
{
text("<object repr() failed>")?;
}
text("\n")?;
} else if !vm.is_none(&unraisable.err_msg) {
writeln!(stderr, "{}:", unraisable.err_msg.str(vm)?);
write(unraisable.err_msg.str(vm)?.into())?;
text(":\n")?;
}

// Print traceback (using actual exc_traceback, not current stack)
if !vm.is_none(&unraisable.exc_traceback) {
let tb_module = vm.import("traceback", 0)?;
let print_tb = tb_module.get_attr("print_tb", vm)?;
let stderr_obj = super::get_stderr(vm)?;
let kwargs: KwArgs = core::iter::once(("file".to_string(), stderr_obj)).collect();
let _ = print_tb.call(
FuncArgs::new(vec![unraisable.exc_traceback.clone()], kwargs),
vm,
);
if vm.try_display_exception_to(&file, &unraisable.exc_value) {
return Ok(());
}

// Check exc_type
if vm.is_none(unraisable.exc_type.as_object()) {
return Ok(());
// Imports can fail during teardown. Keep a native fallback, and keep
// printing the exception even if writing its traceback fails.
if let Ok(traceback) = unraisable
.exc_traceback
.downcast::<crate::builtins::PyTraceback>()
{
let mut writer = crate::py_io::PyWriter(file.clone(), vm);
let _ = crate::exceptions::write_traceback(&mut writer, &traceback);
}
assert!(
unraisable
.exc_type
.fast_issubclass(vm.ctx.exceptions.base_exception_type)
);

// Print module name (if not builtins or __main__).
// A failed lookup or a non-str `__module__` is discarded: print
// `<unknown>` with no trailing `.`, then the qualname.
let module_name = unraisable.exc_type.__module__(vm).ok();

let module_name = unraisable
.exc_type
.as_object()
.get_attr("__module__", vm)
.ok();
if let Some(module_str) = module_name
.as_ref()
.and_then(|name| name.downcast_ref::<PyStr>())
{
let module = module_str.as_wtf8();
if module != "builtins" && module != "__main__" {
write!(stderr, "{module}.");
write(module_str.to_owned().into())?;
text(".")?;
}
} else {
write!(stderr, "<unknown>");
text("<unknown>")?;
}

// Print qualname
let qualname = unraisable.exc_type.__qualname__(vm);
if let Ok(qualname_str) = qualname.downcast::<PyStr>() {
write!(stderr, "{}", qualname_str.as_wtf8());
write(qualname_str.into())?;
} else {
write!(stderr, "{}", unraisable.exc_type.name());
text("<unknown>")?;
}

// Print exception value
if !vm.is_none(&unraisable.exc_value) {
write!(stderr, ": ");
if let Ok(str) = unraisable.exc_value.str(vm) {
write!(stderr, "{}", str.as_wtf8());
} else {
write!(stderr, "<exception str() failed>");
text(": ")?;
if unraisable
.exc_value
.str(vm)
.and_then(|value| write(value.into()))
.is_err()
{
text("<exception str() failed>")?;
}
}
writeln!(stderr);

// Flush stderr
if let Ok(stderr_obj) = super::get_stderr(vm)
&& let Ok(flush) = stderr_obj.get_attr("flush", vm)
{
let _ = flush.call((), vm);
}

Ok(())
text("\n")?;
vm.call_method(&file, "flush", ()).map(drop)
}

#[pyattr]
#[pyfunction(name = "__unraisablehook__")]
fn unraisablehook(unraisable: UnraisableHookArgsData, vm: &VirtualMachine) {
if let Err(e) = _unraisablehook(unraisable, vm) {
let stderr = super::PyStderr(vm);
writeln!(
stderr,
"{}",
e.as_object()
.repr(vm)
.unwrap_or_else(|_| vm.ctx.empty_str.to_owned())
);
}
fn unraisablehook(unraisable: UnraisableHookArgsData, vm: &VirtualMachine) -> PyResult<()> {
write_unraisable(unraisable, vm)
}

#[pyattr]
Expand Down
18 changes: 10 additions & 8 deletions crates/vm/src/vm/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,11 +724,11 @@ impl Interpreter {
/// Finalize vm and turns an exception to exit code.
///
/// Finalization steps (matching Py_FinalizeEx):
/// 1. Flush stdout and stderr.
/// 1. Handle exit exception and turn it to exit code.
/// 1. Call threading._shutdown() to join non-daemon threads.
/// 1. Run atexit exit functions.
/// 1. Set finalizing flag and hang remaining daemon threads.
/// 1. Flush stdout and stderr.
/// 1. Forced GC collection pass (collect cycles while builtins are available).
/// 1. Module finalization (finalize_modules).
/// 1. Clear interpreter-owned cross-interpreter data.
Expand All @@ -748,13 +748,11 @@ impl Interpreter {

fn finalize_inner(self, exit_code: impl FnOnce(&VirtualMachine) -> u32) -> u32 {
self.enter(|vm| {
let flush_status = vm.flush_std();

// See if any exception leaked out:
let exit_code = exit_code(vm);

// Match CPython: if exit_code is 0 and stdout flush failed, exit 120
if vm.finalize_ex(flush_status) < 0 && exit_code == 0 {
// A failed shutdown flush overrides even an explicit nonzero exit code.
if vm.finalize_ex() < 0 {
EXITCODE_FLUSH_FAILURE
} else {
exit_code
Expand All @@ -771,17 +769,17 @@ impl VirtualMachine {
if self.state.thread_is_running_main() {
self.state.set_not_running_main();
}
let status = if self.finalize_ex(self.flush_std()) < 0 {
let status = if self.finalize_ex() < 0 {
EXITCODE_FLUSH_FAILURE
} else {
status
};
crate::host_env::os::exit(status as i32)
}

/// The steps of Py_FinalizeEx after the first flush of stdout and stderr.
/// The steps of Py_FinalizeEx.
/// Return a negative value if any flush failed.
fn finalize_ex(&self, mut flush_status: i32) -> i32 {
fn finalize_ex(&self) -> i32 {
let vm = self;
// Wait for thread shutdown - call threading._shutdown() if available.
// This waits for all non-daemon threads to complete.
Expand Down Expand Up @@ -835,6 +833,10 @@ impl VirtualMachine {
vm.state.finalizing.store(true, Ordering::Release);
}

// Exit callbacks have finished, but modules and the user unraisable
// hook are still available to report a failed stream flush.
let mut flush_status = vm.flush_std();

// GC pass - collect cycles before module cleanup
vm.state.gc.collect_force(2);

Expand Down
Loading