Skip to content

Commit 11c9b0e

Browse files
authored
fix finalizing and atexit timing (#6626)
* fix finalizing and atexit timing * fix shutdown
1 parent 8ce5f49 commit 11c9b0e

File tree

3 files changed

+83
-37
lines changed

3 files changed

+83
-37
lines changed

Lib/test/test_warnings/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,8 +834,6 @@ class CWCmdLineTests(WCmdLineTests, unittest.TestCase):
834834
class PyWCmdLineTests(WCmdLineTests, unittest.TestCase):
835835
module = py_warnings
836836

837-
# TODO: RUSTPYTHON
838-
@unittest.expectedFailure
839837
def test_improper_option(self):
840838
# Same as above, but check that the message is printed out when
841839
# the interpreter is executed. This also checks that options are

crates/vm/src/vm/interpreter.rs

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,11 @@ impl Interpreter {
110110

111111
/// Finalize vm and turns an exception to exit code.
112112
///
113-
/// Finalization steps including 5 steps:
113+
/// Finalization steps (matching Py_FinalizeEx):
114114
/// 1. Flush stdout and stderr.
115115
/// 1. Handle exit exception and turn it to exit code.
116-
/// 1. Wait for non-daemon threads (threading._shutdown).
116+
/// 1. Wait for thread shutdown (call threading._shutdown).
117+
/// 1. Mark vm as finalizing.
117118
/// 1. Run atexit exit functions.
118119
/// 1. Mark vm as finalized.
119120
///
@@ -129,49 +130,33 @@ impl Interpreter {
129130
0
130131
};
131132

132-
// Wait for non-daemon threads (wait_for_thread_shutdown)
133-
wait_for_thread_shutdown(vm);
133+
// Wait for thread shutdown - call threading._shutdown() if available.
134+
// This waits for all non-daemon threads to complete.
135+
// threading module may not be imported, so ignore import errors.
136+
if let Ok(threading) = vm.import("threading", 0)
137+
&& let Ok(shutdown) = threading.get_attr("_shutdown", vm)
138+
&& let Err(e) = shutdown.call((), vm)
139+
{
140+
vm.run_unraisable(
141+
e,
142+
Some("Exception ignored in threading shutdown".to_owned()),
143+
threading,
144+
);
145+
}
146+
147+
// Mark as finalizing AFTER thread shutdown
148+
vm.state.finalizing.store(true, Ordering::Release);
134149

150+
// Run atexit exit functions
135151
atexit::_run_exitfuncs(vm);
136152

137-
vm.state.finalizing.store(true, Ordering::Release);
138-
139153
vm.flush_std();
140154

141155
exit_code
142156
})
143157
}
144158
}
145159

146-
/// Wait until threading._shutdown completes, provided
147-
/// the threading module was imported in the first place.
148-
/// The shutdown routine will wait until all non-daemon
149-
/// "threading" threads have completed.
150-
fn wait_for_thread_shutdown(vm: &VirtualMachine) {
151-
// Try to get the threading module if it was already imported
152-
// Use sys.modules.get("threading") like PyImport_GetModule
153-
let threading = match (|| -> PyResult<_> {
154-
let sys_modules = vm.sys_module.get_attr("modules", vm)?;
155-
let threading = sys_modules.get_item("threading", vm)?;
156-
Ok(threading)
157-
})() {
158-
Ok(module) => module,
159-
Err(_) => {
160-
// threading not imported, nothing to do
161-
return;
162-
}
163-
};
164-
165-
// Call threading._shutdown()
166-
if let Err(e) = vm.call_method(&threading, "_shutdown", ()) {
167-
vm.run_unraisable(
168-
e,
169-
Some("Exception ignored on threading shutdown".to_owned()),
170-
threading,
171-
);
172-
}
173-
}
174-
175160
#[cfg(test)]
176161
mod tests {
177162
use super::*;

crates/vm/src/vm/mod.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,18 @@ impl VirtualMachine {
550550

551551
#[cold]
552552
pub fn run_unraisable(&self, e: PyBaseExceptionRef, msg: Option<String>, object: PyObjectRef) {
553+
// During interpreter finalization, sys.unraisablehook may not be available,
554+
// but we still need to report exceptions (especially from atexit callbacks).
555+
// Write directly to stderr like PyErr_FormatUnraisable.
556+
if self
557+
.state
558+
.finalizing
559+
.load(std::sync::atomic::Ordering::Acquire)
560+
{
561+
self.write_unraisable_to_stderr(&e, msg.as_deref(), &object);
562+
return;
563+
}
564+
553565
let sys_module = self.import("sys", 0).unwrap();
554566
let unraisablehook = sys_module.get_attr("unraisablehook", self).unwrap();
555567

@@ -568,6 +580,57 @@ impl VirtualMachine {
568580
}
569581
}
570582

583+
/// Write unraisable exception to stderr during finalization.
584+
/// Similar to _PyErr_WriteUnraisableDefaultHook in CPython.
585+
fn write_unraisable_to_stderr(
586+
&self,
587+
e: &PyBaseExceptionRef,
588+
msg: Option<&str>,
589+
object: &PyObjectRef,
590+
) {
591+
// Get stderr once and reuse it
592+
let stderr = crate::stdlib::sys::get_stderr(self).ok();
593+
594+
let write_to_stderr = |s: &str, stderr: &Option<PyObjectRef>, vm: &VirtualMachine| {
595+
if let Some(stderr) = stderr {
596+
let _ = vm.call_method(stderr, "write", (s.to_owned(),));
597+
} else {
598+
eprint!("{}", s);
599+
}
600+
};
601+
602+
// Format: "Exception ignored {msg} {object_repr}\n"
603+
if let Some(msg) = msg {
604+
write_to_stderr(&format!("Exception ignored {}", msg), &stderr, self);
605+
} else {
606+
write_to_stderr("Exception ignored in: ", &stderr, self);
607+
}
608+
609+
if let Ok(repr) = object.repr(self) {
610+
write_to_stderr(&format!("{}\n", repr.as_str()), &stderr, self);
611+
} else {
612+
write_to_stderr("<object repr failed>\n", &stderr, self);
613+
}
614+
615+
// Write exception type and message
616+
let exc_type_name = e.class().name();
617+
if let Ok(exc_str) = e.as_object().str(self) {
618+
let exc_str = exc_str.as_str();
619+
if exc_str.is_empty() {
620+
write_to_stderr(&format!("{}\n", exc_type_name), &stderr, self);
621+
} else {
622+
write_to_stderr(&format!("{}: {}\n", exc_type_name, exc_str), &stderr, self);
623+
}
624+
} else {
625+
write_to_stderr(&format!("{}\n", exc_type_name), &stderr, self);
626+
}
627+
628+
// Flush stderr to ensure output is visible
629+
if let Some(ref stderr) = stderr {
630+
let _ = self.call_method(stderr, "flush", ());
631+
}
632+
}
633+
571634
#[inline(always)]
572635
pub fn run_frame(&self, frame: FrameRef) -> PyResult {
573636
match self.with_frame(frame, |f| f.run(self))? {

0 commit comments

Comments
 (0)