From 360637fe0c4a691ba62a5481d9b2f115ba93dd83 Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 00:59:51 +0300 Subject: [PATCH 01/16] patch --- Lib/test/test_pdb.py | 36 ++++++++++++++++++- ...-08-26-00-58-26.gh-issue-123321.ApxcnE.rst | 2 ++ Parser/myreadline.c | 11 +++--- 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-08-26-00-58-26.gh-issue-123321.ApxcnE.rst diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index db7d1b1e9cd935..8788bb5e50e6e4 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2731,6 +2731,40 @@ def test_pdb_issue_gh_103225(): (Pdb) continue """ +if not SKIP_ASYNCIO_TESTS: + @unittest.skipUnless(sys.platform == 'win32', 'Windows specific test') + def test_pdb_issue_gh_123321(): + """See GH-123321 + + Make sure pdb does not segfault on asyncio threads on Windows. + + >>> import asyncio + + >>> async def test_main(): + ... def inner(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... pass + ... task1 = asyncio.create_task(asyncio.to_thread(inner)) + ... task2 = asyncio.create_task(asyncio.to_thread(inner)) + ... await asyncio.gather(task1, task2) + + >>> def test_function(): + ... loop = asyncio.new_event_loop() + ... loop.run_until_complete(test_main()) + ... loop.close() + ... asyncio.set_event_loop_policy(None) + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'return', + ... 'next', + ... 'return', + ... 'next', + ... 'next', + ... 'continue', + ... ]): + fillme + """ + def test_pdb_issue_gh_101517(): """See GH-101517 @@ -2765,7 +2799,7 @@ def test_pdb_issue_gh_108976(): ... test_function() > (4)test_function() -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - (Pdb) continue + (Pdb) continasdue """ diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-08-26-00-58-26.gh-issue-123321.ApxcnE.rst b/Misc/NEWS.d/next/Core and Builtins/2024-08-26-00-58-26.gh-issue-123321.ApxcnE.rst new file mode 100644 index 00000000000000..b0547e0e588e3d --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-08-26-00-58-26.gh-issue-123321.ApxcnE.rst @@ -0,0 +1,2 @@ +Prevent Parser/myreadline race condition from segfaulting on multi-threaded +use. Patch by Bar Harel and Amit Wienner. diff --git a/Parser/myreadline.c b/Parser/myreadline.c index 1825665354844b..34201f8d042d0b 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -392,9 +392,11 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } } - _PyOS_ReadlineTState = tstate; + Py_BEGIN_ALLOW_THREADS PyThread_acquire_lock(_PyOS_ReadlineLock, 1); + _PyOS_ReadlineTState = tstate; + /* This is needed to handle the unlikely case that the * interpreter is in interactive mode *and* stdin/out are not @@ -418,11 +420,10 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) else { rv = (*PyOS_ReadlineFunctionPointer)(sys_stdin, sys_stdout, prompt); } - Py_END_ALLOW_THREADS - - PyThread_release_lock(_PyOS_ReadlineLock); - + _PyOS_ReadlineTState = NULL; + PyThread_release_lock(_PyOS_ReadlineLock); + Py_END_ALLOW_THREADS if (rv == NULL) return NULL; From 8f16d5aa9998025b766ec83d0ca6ea809a3d6acf Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 01:11:58 +0300 Subject: [PATCH 02/16] whitespace --- Parser/myreadline.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parser/myreadline.c b/Parser/myreadline.c index 34201f8d042d0b..ba725b7611edaf 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -420,7 +420,7 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) else { rv = (*PyOS_ReadlineFunctionPointer)(sys_stdin, sys_stdout, prompt); } - + _PyOS_ReadlineTState = NULL; PyThread_release_lock(_PyOS_ReadlineLock); Py_END_ALLOW_THREADS From 1f5c558b3c38ac805cb456815d619cbda8b2c1ec Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 01:15:02 +0300 Subject: [PATCH 03/16] hmmm pdb doctests don't run --- Lib/test/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 8788bb5e50e6e4..b69215d5576faf 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2799,7 +2799,7 @@ def test_pdb_issue_gh_108976(): ... test_function() > (4)test_function() -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - (Pdb) continasdue + (Pdb) continue """ From 8a8c355101b11bbee9c4a732ebce98383ae95cc4 Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 01:24:54 +0300 Subject: [PATCH 04/16] for some reason the doctests don't run locally --- Lib/test/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index b69215d5576faf..aac1adc7ac9b1d 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2732,7 +2732,6 @@ def test_pdb_issue_gh_103225(): """ if not SKIP_ASYNCIO_TESTS: - @unittest.skipUnless(sys.platform == 'win32', 'Windows specific test') def test_pdb_issue_gh_123321(): """See GH-123321 @@ -2762,6 +2761,7 @@ def test_pdb_issue_gh_123321(): ... 'next', ... 'continue', ... ]): + ... test_function() fillme """ From ed58ecb53d27d1481d4acacdadf46ddaa2955c46 Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 01:42:41 +0300 Subject: [PATCH 05/16] doctest fix --- Lib/test/test_pdb.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index aac1adc7ac9b1d..ef0868e1eb2583 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2753,7 +2753,7 @@ def test_pdb_issue_gh_123321(): ... loop.close() ... asyncio.set_event_loop_policy(None) - >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS ... 'return', ... 'next', ... 'return', @@ -2762,7 +2762,27 @@ def test_pdb_issue_gh_123321(): ... 'continue', ... ]): ... test_function() - fillme + > (3)inner() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) return + --Return-- + > (4)inner()->None + -> pass + (Pdb) next + > (3)inner() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) return + --Return-- + > (4)inner()->None + -> pass + (Pdb) next + > ... + -> self.future.set_result(result) + (Pdb) next + --Return-- + > ... + -> self.future.set_result(result) + (Pdb) continue """ def test_pdb_issue_gh_101517(): From db92111ec4234d066e8578e707a6015a628c9fd1 Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 02:12:19 +0300 Subject: [PATCH 06/16] mmm --- Lib/test/test_pdb.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index ef0868e1eb2583..3702e5425cabe3 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2760,27 +2760,31 @@ def test_pdb_issue_gh_123321(): ... 'next', ... 'next', ... 'continue', + ... 'continue', ... ]): ... test_function() > (3)inner() -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() (Pdb) return - --Return-- - > (4)inner()->None - -> pass - (Pdb) next > (3)inner() -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - (Pdb) return + (Pdb) next --Return-- > (4)inner()->None -> pass + (Pdb) return + > (4)inner() + -> pass (Pdb) next - > ... + --Return-- + ... -> self.future.set_result(result) (Pdb) next + > (4)inner()->None + -> pass + (Pdb) continue --Return-- - > ... + ... -> self.future.set_result(result) (Pdb) continue """ From b991cd6a1098b5d3c1ec0dcac830af9e09175efc Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 02:23:51 +0300 Subject: [PATCH 07/16] this will probably work. --- Lib/test/test_pdb.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 3702e5425cabe3..1cccd3b7648ea7 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2757,35 +2757,18 @@ def test_pdb_issue_gh_123321(): ... 'return', ... 'next', ... 'return', - ... 'next', - ... 'next', - ... 'continue', - ... 'continue', + ... 'next', # Tehnically all we care about are the continue + ... 'next', # statements. The rest is to trigger the segfault. + ... 'continue', # Can't be accurate with output + ... 'continue', # due to multithreading. ... ]): ... test_function() > (3)inner() -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() (Pdb) return - > (3)inner() - -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - (Pdb) next - --Return-- - > (4)inner()->None - -> pass - (Pdb) return - > (4)inner() - -> pass - (Pdb) next - --Return-- ... - -> self.future.set_result(result) - (Pdb) next - > (4)inner()->None - -> pass (Pdb) continue - --Return-- ... - -> self.future.set_result(result) (Pdb) continue """ From 26ad6bcdc7edff16e9867f66ed9b0fae7feb1f1a Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 02:26:00 +0300 Subject: [PATCH 08/16] typo --- Lib/test/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 1cccd3b7648ea7..6aacb97d448021 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2757,7 +2757,7 @@ def test_pdb_issue_gh_123321(): ... 'return', ... 'next', ... 'return', - ... 'next', # Tehnically all we care about are the continue + ... 'next', # Technically all we care about are the continue ... 'next', # statements. The rest is to trigger the segfault. ... 'continue', # Can't be accurate with output ... 'continue', # due to multithreading. From 03243d085d3a03b7fd3698cbaee3f8a62b3fa9cb Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Mon, 26 Aug 2024 02:44:43 +0300 Subject: [PATCH 09/16] ugh --- Lib/test/test_pdb.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 6aacb97d448021..308413ed2579bf 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2767,8 +2767,7 @@ def test_pdb_issue_gh_123321(): -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() (Pdb) return ... - (Pdb) continue - ... + (Pdb) continue... (Pdb) continue """ From 6b0b820363095284730b99fb04858c0fc4970fab Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Wed, 28 Aug 2024 16:59:59 +0300 Subject: [PATCH 10/16] small check for free-threaded build --- Parser/myreadline.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Parser/myreadline.c b/Parser/myreadline.c index ba725b7611edaf..c0af7d1416f3a4 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -393,8 +393,8 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } - Py_BEGIN_ALLOW_THREADS PyThread_acquire_lock(_PyOS_ReadlineLock, 1); + Py_BEGIN_ALLOW_THREADS _PyOS_ReadlineTState = tstate; @@ -422,8 +422,8 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } _PyOS_ReadlineTState = NULL; - PyThread_release_lock(_PyOS_ReadlineLock); Py_END_ALLOW_THREADS + PyThread_release_lock(_PyOS_ReadlineLock); if (rv == NULL) return NULL; From 40efaacbc2f35bcd33029cb24c7b1fd95148882c Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Fri, 30 Aug 2024 23:12:00 +0300 Subject: [PATCH 11/16] Reverting to previous solution as the free-threaded build fix deadlocks. Shifted tests to the readline module. --- Lib/test/test_readline.py | 27 +++++++++++++++++++++++++++ Parser/myreadline.c | 15 ++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 91fd7dd13f9063..3dfbd9d5dbebf7 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -12,6 +12,7 @@ from test.support.os_helper import unlink, temp_dir, TESTFN from test.support.pty_helper import run_pty from test.support.script_helper import assert_python_ok +from test.support.threading_helper import requires_working_threading # Skip tests if there is no readline module readline = import_module('readline') @@ -349,6 +350,32 @@ def test_history_size(self): self.assertEqual(len(lines), history_size) self.assertEqual(lines[-1].strip(), b"last input") + @requires_working_threading() + def test_gh123321_threadsafe(self): + """gh-123321: readline should be thread-safe and not crash""" + script = r""" +import threading + +def func(): + input() + +thread1 = threading.Thread(target=func) +thread2 = threading.Thread(target=func) +thread1.start() +thread2.start() +thread1.join(3) +thread2.join(3) +if thread1.is_alive() or thread2.is_alive(): + print("Thread is still alive") +else: + print("done") +""" + + output = run_pty(script, input=b"input1\rinput2\r") + + self.assertIn(b"done", output) + + def test_write_read_limited_history(self): previous_length = readline.get_history_length() self.addCleanup(readline.set_history_length, previous_length) diff --git a/Parser/myreadline.c b/Parser/myreadline.c index c0af7d1416f3a4..b6b1e7e608630e 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -392,10 +392,16 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } } - - PyThread_acquire_lock(_PyOS_ReadlineLock, 1); Py_BEGIN_ALLOW_THREADS + + // GH-123321: We need to acquire the lock before setting + // _PyOS_ReadlineTState and after the release of the GIL, otherwise + // the variable may be nullified by a different thread or a deadlock + // may occur if the GIL is taken in any sub-function. + PyThread_acquire_lock(_PyOS_ReadlineLock, 1); _PyOS_ReadlineTState = tstate; + + /* This is needed to handle the unlikely case that the @@ -421,9 +427,12 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) rv = (*PyOS_ReadlineFunctionPointer)(sys_stdin, sys_stdout, prompt); } + // gh-123321: Must set the variable and then release the lock before + // taking the GIL. Otherwise a deadlock or segfault may occur. _PyOS_ReadlineTState = NULL; - Py_END_ALLOW_THREADS PyThread_release_lock(_PyOS_ReadlineLock); + + Py_END_ALLOW_THREADS if (rv == NULL) return NULL; From fc40da3bf7a011282be84d833989059d5b89a90f Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Fri, 30 Aug 2024 23:16:46 +0300 Subject: [PATCH 12/16] lint --- Parser/myreadline.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Parser/myreadline.c b/Parser/myreadline.c index b6b1e7e608630e..6eab56ac52dc08 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -393,16 +393,13 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } Py_BEGIN_ALLOW_THREADS - + // GH-123321: We need to acquire the lock before setting // _PyOS_ReadlineTState and after the release of the GIL, otherwise // the variable may be nullified by a different thread or a deadlock // may occur if the GIL is taken in any sub-function. PyThread_acquire_lock(_PyOS_ReadlineLock, 1); _PyOS_ReadlineTState = tstate; - - - /* This is needed to handle the unlikely case that the * interpreter is in interactive mode *and* stdin/out are not @@ -431,7 +428,7 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) // taking the GIL. Otherwise a deadlock or segfault may occur. _PyOS_ReadlineTState = NULL; PyThread_release_lock(_PyOS_ReadlineLock); - + Py_END_ALLOW_THREADS if (rv == NULL) From bebc58083d33888f2451c0c20a463b56d99904bf Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Fri, 30 Aug 2024 23:27:43 +0300 Subject: [PATCH 13/16] remove pdb tests --- Lib/test/test_pdb.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 308413ed2579bf..db7d1b1e9cd935 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2731,46 +2731,6 @@ def test_pdb_issue_gh_103225(): (Pdb) continue """ -if not SKIP_ASYNCIO_TESTS: - def test_pdb_issue_gh_123321(): - """See GH-123321 - - Make sure pdb does not segfault on asyncio threads on Windows. - - >>> import asyncio - - >>> async def test_main(): - ... def inner(): - ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - ... pass - ... task1 = asyncio.create_task(asyncio.to_thread(inner)) - ... task2 = asyncio.create_task(asyncio.to_thread(inner)) - ... await asyncio.gather(task1, task2) - - >>> def test_function(): - ... loop = asyncio.new_event_loop() - ... loop.run_until_complete(test_main()) - ... loop.close() - ... asyncio.set_event_loop_policy(None) - - >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS - ... 'return', - ... 'next', - ... 'return', - ... 'next', # Technically all we care about are the continue - ... 'next', # statements. The rest is to trigger the segfault. - ... 'continue', # Can't be accurate with output - ... 'continue', # due to multithreading. - ... ]): - ... test_function() - > (3)inner() - -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - (Pdb) return - ... - (Pdb) continue... - (Pdb) continue - """ - def test_pdb_issue_gh_101517(): """See GH-101517 From 0379488bee876ded34de694535bbb2a265952152 Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Fri, 30 Aug 2024 23:34:14 +0300 Subject: [PATCH 14/16] Add requires gil enabled tag --- Lib/test/test_readline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 3dfbd9d5dbebf7..350eadf10c012d 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -7,7 +7,7 @@ import tempfile import textwrap import unittest -from test.support import verbose +from test.support import requires_gil_enabled, verbose from test.support.import_helper import import_module from test.support.os_helper import unlink, temp_dir, TESTFN from test.support.pty_helper import run_pty @@ -351,6 +351,7 @@ def test_history_size(self): self.assertEqual(lines[-1].strip(), b"last input") @requires_working_threading() + @requires_gil_enabled() def test_gh123321_threadsafe(self): """gh-123321: readline should be thread-safe and not crash""" script = r""" From fd2929cce0c1e6f0f4aeeda855c4dbeefbee3055 Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Sat, 31 Aug 2024 00:23:29 +0300 Subject: [PATCH 15/16] CR fixes. --- Lib/test/test_readline.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 350eadf10c012d..a04940f3f61d9f 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -354,23 +354,21 @@ def test_history_size(self): @requires_gil_enabled() def test_gh123321_threadsafe(self): """gh-123321: readline should be thread-safe and not crash""" - script = r""" -import threading - -def func(): - input() - -thread1 = threading.Thread(target=func) -thread2 = threading.Thread(target=func) -thread1.start() -thread2.start() -thread1.join(3) -thread2.join(3) -if thread1.is_alive() or thread2.is_alive(): - print("Thread is still alive") -else: - print("done") -""" + script = textwrap.dedent(r""" + import threading + from test.support.threading_helper import join_thread + + def func(): + input() + + thread1 = threading.Thread(target=func) + thread2 = threading.Thread(target=func) + thread1.start() + thread2.start() + join_thread(thread1) + join_thread(thread2) + print("done") + """) output = run_pty(script, input=b"input1\rinput2\r") From 93dd0233b8db89f3390a7d6addb8c667f5f0f6cc Mon Sep 17 00:00:00 2001 From: "bar.harel" Date: Sat, 31 Aug 2024 01:01:50 +0300 Subject: [PATCH 16/16] . --- Lib/test/test_readline.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index a04940f3f61d9f..7d07906df20cc1 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -355,19 +355,19 @@ def test_history_size(self): def test_gh123321_threadsafe(self): """gh-123321: readline should be thread-safe and not crash""" script = textwrap.dedent(r""" - import threading - from test.support.threading_helper import join_thread - - def func(): - input() - - thread1 = threading.Thread(target=func) - thread2 = threading.Thread(target=func) - thread1.start() - thread2.start() - join_thread(thread1) - join_thread(thread2) - print("done") + import threading + from test.support.threading_helper import join_thread + + def func(): + input() + + thread1 = threading.Thread(target=func) + thread2 = threading.Thread(target=func) + thread1.start() + thread2.start() + join_thread(thread1) + join_thread(thread2) + print("done") """) output = run_pty(script, input=b"input1\rinput2\r")