summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/script_helper.py18
-rw-r--r--Lib/test/test_io.py45
-rw-r--r--Lib/test/test_script_helper.py17
-rw-r--r--Misc/NEWS5
-rw-r--r--Modules/_io/bufferedio.c23
5 files changed, 94 insertions, 14 deletions
diff --git a/Lib/test/script_helper.py b/Lib/test/script_helper.py
index 8743dba..7ac19bf 100644
--- a/Lib/test/script_helper.py
+++ b/Lib/test/script_helper.py
@@ -1,6 +1,7 @@
# Common utility functions used by various script execution tests
# e.g. test_cmd_line, test_cmd_line_script and test_runpy
+import collections
import importlib
import sys
import os
@@ -50,8 +51,12 @@ def interpreter_requires_environment():
return __cached_interp_requires_environment
+_PythonRunResult = collections.namedtuple("_PythonRunResult",
+ ("rc", "out", "err"))
+
+
# Executing the interpreter in a subprocess
-def _assert_python(expected_success, *args, **env_vars):
+def run_python_until_end(*args, **env_vars):
env_required = interpreter_requires_environment()
if '__isolated' in env_vars:
isolated = env_vars.pop('__isolated')
@@ -85,9 +90,14 @@ def _assert_python(expected_success, *args, **env_vars):
p.stderr.close()
rc = p.returncode
err = strip_python_stderr(err)
- if (rc and expected_success) or (not rc and not expected_success):
+ return _PythonRunResult(rc, out, err), cmd_line
+
+def _assert_python(expected_success, *args, **env_vars):
+ res, cmd_line = run_python_until_end(*args, **env_vars)
+ if (res.rc and expected_success) or (not res.rc and not expected_success):
# Limit to 80 lines to ASCII characters
maxlen = 80 * 100
+ out, err = res.out, res.err
if len(out) > maxlen:
out = b'(... truncated stdout ...)' + out[-maxlen:]
if len(err) > maxlen:
@@ -106,10 +116,10 @@ def _assert_python(expected_success, *args, **env_vars):
"---\n"
"%s\n"
"---"
- % (rc, cmd_line,
+ % (res.rc, cmd_line,
out,
err))
- return rc, out, err
+ return res
def assert_python_ok(*args, **env_vars):
"""
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index 9fb85f2..2d02a31 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -35,7 +35,7 @@ import weakref
from collections import deque, UserList
from itertools import cycle, count
from test import support
-from test.script_helper import assert_python_ok
+from test.script_helper import assert_python_ok, run_python_until_end
import codecs
import io # C implementation of io
@@ -3437,6 +3437,49 @@ class CMiscIOTest(MiscIOTest):
b = bytearray(2)
self.assertRaises(ValueError, bufio.readinto, b)
+ @unittest.skipUnless(threading, 'Threading required for this test.')
+ def check_daemon_threads_shutdown_deadlock(self, stream_name):
+ # Issue #23309: deadlocks at shutdown should be avoided when a
+ # daemon thread and the main thread both write to a file.
+ code = """if 1:
+ import sys
+ import time
+ import threading
+
+ file = sys.{stream_name}
+
+ def run():
+ while True:
+ file.write('.')
+ file.flush()
+
+ thread = threading.Thread(target=run)
+ thread.daemon = True
+ thread.start()
+
+ time.sleep(0.5)
+ file.write('!')
+ file.flush()
+ """.format_map(locals())
+ res, _ = run_python_until_end("-c", code)
+ err = res.err.decode()
+ if res.rc != 0:
+ # Failure: should be a fatal error
+ self.assertIn("Fatal Python error: could not acquire lock "
+ "for <_io.BufferedWriter name='<{stream_name}>'> "
+ "at interpreter shutdown, possibly due to "
+ "daemon threads".format_map(locals()),
+ err)
+ else:
+ self.assertFalse(err.strip('.!'))
+
+ def test_daemon_threads_shutdown_stdout_deadlock(self):
+ self.check_daemon_threads_shutdown_deadlock('stdout')
+
+ def test_daemon_threads_shutdown_stderr_deadlock(self):
+ self.check_daemon_threads_shutdown_deadlock('stderr')
+
+
class PyMiscIOTest(MiscIOTest):
io = pyio
diff --git a/Lib/test/test_script_helper.py b/Lib/test/test_script_helper.py
index 7f5d654..8694530 100644
--- a/Lib/test/test_script_helper.py
+++ b/Lib/test/test_script_helper.py
@@ -8,26 +8,27 @@ from unittest import mock
class TestScriptHelper(unittest.TestCase):
- def test_assert_python_expect_success(self):
- t = script_helper._assert_python(True, '-c', 'import sys; sys.exit(0)')
+
+ def test_assert_python_ok(self):
+ t = script_helper.assert_python_ok('-c', 'import sys; sys.exit(0)')
self.assertEqual(0, t[0], 'return code was not 0')
- def test_assert_python_expect_failure(self):
+ def test_assert_python_failure(self):
# I didn't import the sys module so this child will fail.
- rc, out, err = script_helper._assert_python(False, '-c', 'sys.exit(0)')
+ rc, out, err = script_helper.assert_python_failure('-c', 'sys.exit(0)')
self.assertNotEqual(0, rc, 'return code should not be 0')
- def test_assert_python_raises_expect_success(self):
+ def test_assert_python_ok_raises(self):
# I didn't import the sys module so this child will fail.
with self.assertRaises(AssertionError) as error_context:
- script_helper._assert_python(True, '-c', 'sys.exit(0)')
+ script_helper.assert_python_ok('-c', 'sys.exit(0)')
error_msg = str(error_context.exception)
self.assertIn('command line:', error_msg)
self.assertIn('sys.exit(0)', error_msg, msg='unexpected command line')
- def test_assert_python_raises_expect_failure(self):
+ def test_assert_python_failure_raises(self):
with self.assertRaises(AssertionError) as error_context:
- script_helper._assert_python(False, '-c', 'import sys; sys.exit(0)')
+ script_helper.assert_python_failure('-c', 'import sys; sys.exit(0)')
error_msg = str(error_context.exception)
self.assertIn('Process return code is 0\n', error_msg)
self.assertIn('import sys; sys.exit(0)', error_msg,
diff --git a/Misc/NEWS b/Misc/NEWS
index f925f1c..8cb2cad 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,11 @@ Release date: XXX
Core and Builtins
-----------------
+- Issue #23309: Avoid a deadlock at shutdown if a daemon thread is aborted
+ while it is holding a lock to a buffered I/O object, and the main thread
+ tries to use the same I/O object (typically stdout or stderr). A fatal
+ error is emitted instead.
+
- Issue #22977: Fixed formatting Windows error messages on Wine.
Patch by Martin Panter.
diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c
index ea9f533..7cf5dda 100644
--- a/Modules/_io/bufferedio.c
+++ b/Modules/_io/bufferedio.c
@@ -318,14 +318,35 @@ typedef struct {
static int
_enter_buffered_busy(buffered *self)
{
+ int relax_locking;
+ PyLockStatus st;
if (self->owner == PyThread_get_thread_ident()) {
PyErr_Format(PyExc_RuntimeError,
"reentrant call inside %R", self);
return 0;
}
+ relax_locking = (_Py_Finalizing != NULL);
Py_BEGIN_ALLOW_THREADS
- PyThread_acquire_lock(self->lock, 1);
+ if (!relax_locking)
+ st = PyThread_acquire_lock(self->lock, 1);
+ else {
+ /* When finalizing, we don't want a deadlock to happen with daemon
+ * threads abruptly shut down while they owned the lock.
+ * Therefore, only wait for a grace period (1 s.).
+ * Note that non-daemon threads have already exited here, so this
+ * shouldn't affect carefully written threaded I/O code.
+ */
+ st = PyThread_acquire_lock_timed(self->lock, 1e6, 0);
+ }
Py_END_ALLOW_THREADS
+ if (relax_locking && st != PY_LOCK_ACQUIRED) {
+ PyObject *msgobj = PyUnicode_FromFormat(
+ "could not acquire lock for %A at interpreter "
+ "shutdown, possibly due to daemon threads",
+ (PyObject *) self);
+ char *msg = PyUnicode_AsUTF8(msgobj);
+ Py_FatalError(msg);
+ }
return 1;
}