From 44cd8d53a13ea016b573d6fc3223389e18953744 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 25 Mar 2026 18:59:16 +0800 Subject: [PATCH] fix: Flush stdout on shutdown matching CPython behavior (#7503) * fix: flush stdout on interpreter shutdown matching CPython behavior When stdout flush fails during shutdown, report the error via run_unraisable and exit with code 120 (matching CPython's Py_FinalizeEx). Skip flushing already-closed or None streams. Stderr flush errors remain silently ignored per CPython behavior. Fixes #5521 Signed-off-by: majiayu000 <1835304752@qq.com> * refactor: replace magic number 120 with named constant EXITCODE_FLUSH_FAILURE Address review feedback on PR #7503: improve readability by extracting the CPython-compat exit code into a named constant. Signed-off-by: majiayu000 <1835304752@qq.com> --------- Signed-off-by: majiayu000 <1835304752@qq.com> --- Lib/test/test_cmd_line.py | 2 -- crates/vm/src/vm/interpreter.rs | 17 ++++++++++++++--- crates/vm/src/vm/vm_object.rs | 25 +++++++++++++++++++++---- 3 files changed, 35 insertions(+), 9 deletions(-) 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]