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>
This commit is contained in:
lif authored and GitHub committed 2026-03-25 19:59:16 +09:00
1 parent ea5a6cd9c0
commit e1ecb87f32
3 files changed
+35 -9

No files matched your search

-2
View File
@@ -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.
+14 -3
View File
@@ -10,6 +10,10 @@ use core::sync::atomic::Ordering;
type InitFunc = Box<dyn FnOnce(&mut VirtualMachine)>;
/// 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<PyBaseExceptionRef>) -> 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
}
})
}
}
+21 -4
View File
@@ -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]