diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index eb455ebae..8aa6dad27 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -475,8 +475,6 @@ class CmdLineTest(unittest.TestCase): self.assertRegex(err.decode('ascii', 'ignore'), 'SyntaxError') self.assertEqual(b'', out) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_stdout_flush_at_shutdown(self): # Issue #5319: if stdout.flush() fails at shutdown, an error should # be printed out. diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index 5bf7436e9..57f3d4064 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -10,6 +10,10 @@ use core::sync::atomic::Ordering; type InitFunc = Box; +/// Exit code used when stdout/stderr flush fails during interpreter shutdown. +/// Matches CPython's behavior (see cpython/Python/pylifecycle.c). +const EXITCODE_FLUSH_FAILURE: u32 = 120; + /// Configuration builder for constructing an Interpreter. /// /// This is the preferred way to configure and create an interpreter with custom modules. @@ -401,7 +405,7 @@ impl Interpreter { /// Note that calling `finalize` is not necessary by purpose though. pub fn finalize(self, exc: Option) -> u32 { self.enter(|vm| { - vm.flush_std(); + let mut flush_status = vm.flush_std(); // See if any exception leaked out: let exit_code = if let Some(exc) = exc { @@ -439,9 +443,16 @@ impl Interpreter { // (while builtins is still available for __del__), then clear module dicts. vm.finalize_modules(); - vm.flush_std(); + if vm.flush_std() < 0 && flush_status == 0 { + flush_status = -1; + } - exit_code + // Match CPython: if exit_code is 0 and stdout flush failed, exit 120 + if exit_code == 0 && flush_status < 0 { + EXITCODE_FLUSH_FAILURE + } else { + exit_code + } }) } } diff --git a/crates/vm/src/vm/vm_object.rs b/crates/vm/src/vm/vm_object.rs index 0d5b28614..0c60be2c0 100644 --- a/crates/vm/src/vm/vm_object.rs +++ b/crates/vm/src/vm/vm_object.rs @@ -52,14 +52,31 @@ impl VirtualMachine { } } - pub(crate) fn flush_std(&self) { + /// Returns true if the file object's `closed` attribute is truthy. + fn file_is_closed(&self, file: &PyObject) -> bool { + file.get_attr("closed", self) + .ok() + .is_some_and(|v| v.try_to_bool(self).unwrap_or(false)) + } + + pub(crate) fn flush_std(&self) -> i32 { let vm = self; - if let Ok(stdout) = sys::get_stdout(vm) { - let _ = vm.call_method(&stdout, identifier!(vm, flush).as_str(), ()); + let mut status = 0; + if let Ok(stdout) = sys::get_stdout(vm) + && !vm.is_none(&stdout) + && !vm.file_is_closed(&stdout) + && let Err(e) = vm.call_method(&stdout, identifier!(vm, flush).as_str(), ()) + { + vm.run_unraisable(e, None, stdout); + status = -1; } - if let Ok(stderr) = sys::get_stderr(vm) { + if let Ok(stderr) = sys::get_stderr(vm) + && !vm.is_none(&stderr) + && !vm.file_is_closed(&stderr) + { let _ = vm.call_method(&stderr, identifier!(vm, flush).as_str(), ()); } + status } #[track_caller]