Skip to content

Commit e1ecb87

Browse files
authored
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>
1 parent ea5a6cd commit e1ecb87

File tree

3 files changed

+35
-9
lines changed

3 files changed

+35
-9
lines changed

Lib/test/test_cmd_line.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,8 +475,6 @@ def test_unmached_quote(self):
475475
self.assertRegex(err.decode('ascii', 'ignore'), 'SyntaxError')
476476
self.assertEqual(b'', out)
477477

478-
# TODO: RUSTPYTHON
479-
@unittest.expectedFailure
480478
def test_stdout_flush_at_shutdown(self):
481479
# Issue #5319: if stdout.flush() fails at shutdown, an error should
482480
# be printed out.

crates/vm/src/vm/interpreter.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ use core::sync::atomic::Ordering;
1010

1111
type InitFunc = Box<dyn FnOnce(&mut VirtualMachine)>;
1212

13+
/// Exit code used when stdout/stderr flush fails during interpreter shutdown.
14+
/// Matches CPython's behavior (see cpython/Python/pylifecycle.c).
15+
const EXITCODE_FLUSH_FAILURE: u32 = 120;
16+
1317
/// Configuration builder for constructing an Interpreter.
1418
///
1519
/// This is the preferred way to configure and create an interpreter with custom modules.
@@ -401,7 +405,7 @@ impl Interpreter {
401405
/// Note that calling `finalize` is not necessary by purpose though.
402406
pub fn finalize(self, exc: Option<PyBaseExceptionRef>) -> u32 {
403407
self.enter(|vm| {
404-
vm.flush_std();
408+
let mut flush_status = vm.flush_std();
405409

406410
// See if any exception leaked out:
407411
let exit_code = if let Some(exc) = exc {
@@ -439,9 +443,16 @@ impl Interpreter {
439443
// (while builtins is still available for __del__), then clear module dicts.
440444
vm.finalize_modules();
441445

442-
vm.flush_std();
446+
if vm.flush_std() < 0 && flush_status == 0 {
447+
flush_status = -1;
448+
}
443449

444-
exit_code
450+
// Match CPython: if exit_code is 0 and stdout flush failed, exit 120
451+
if exit_code == 0 && flush_status < 0 {
452+
EXITCODE_FLUSH_FAILURE
453+
} else {
454+
exit_code
455+
}
445456
})
446457
}
447458
}

crates/vm/src/vm/vm_object.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,31 @@ impl VirtualMachine {
5252
}
5353
}
5454

55-
pub(crate) fn flush_std(&self) {
55+
/// Returns true if the file object's `closed` attribute is truthy.
56+
fn file_is_closed(&self, file: &PyObject) -> bool {
57+
file.get_attr("closed", self)
58+
.ok()
59+
.is_some_and(|v| v.try_to_bool(self).unwrap_or(false))
60+
}
61+
62+
pub(crate) fn flush_std(&self) -> i32 {
5663
let vm = self;
57-
if let Ok(stdout) = sys::get_stdout(vm) {
58-
let _ = vm.call_method(&stdout, identifier!(vm, flush).as_str(), ());
64+
let mut status = 0;
65+
if let Ok(stdout) = sys::get_stdout(vm)
66+
&& !vm.is_none(&stdout)
67+
&& !vm.file_is_closed(&stdout)
68+
&& let Err(e) = vm.call_method(&stdout, identifier!(vm, flush).as_str(), ())
69+
{
70+
vm.run_unraisable(e, None, stdout);
71+
status = -1;
5972
}
60-
if let Ok(stderr) = sys::get_stderr(vm) {
73+
if let Ok(stderr) = sys::get_stderr(vm)
74+
&& !vm.is_none(&stderr)
75+
&& !vm.file_is_closed(&stderr)
76+
{
6177
let _ = vm.call_method(&stderr, identifier!(vm, flush).as_str(), ());
6278
}
79+
status
6380
}
6481

6582
#[track_caller]

0 commit comments

Comments
 (0)