|
35 | 35 | import queue
|
36 | 36 | import random
|
37 | 37 | import re
|
| 38 | +import signal |
38 | 39 | import socket
|
39 | 40 | import struct
|
40 | 41 | import sys
|
@@ -666,6 +667,72 @@ def remove_loop(fname, tries):
|
666 | 667 | if os.path.exists(fn):
|
667 | 668 | os.unlink(fn)
|
668 | 669 |
|
| 670 | + # The implementation relies on os.register_at_fork existing, but we test |
| 671 | + # based on os.fork existing because that is what users and this test use. |
| 672 | + # This helps ensure that when fork exists (the important concept) that the |
| 673 | + # register_at_fork mechanism is also present and used. |
| 674 | + @unittest.skipIf(not hasattr(os, 'fork'), 'Test requires os.fork().') |
| 675 | + def test_post_fork_child_no_deadlock(self): |
| 676 | + """Ensure forked child logging locks are not held; bpo-6721.""" |
| 677 | + refed_h = logging.Handler() |
| 678 | + refed_h.name = 'because we need at least one for this test' |
| 679 | + self.assertGreater(len(logging._handlers), 0) |
| 680 | + |
| 681 | + locks_held__ready_to_fork = threading.Event() |
| 682 | + fork_happened__release_locks_and_end_thread = threading.Event() |
| 683 | + |
| 684 | + def lock_holder_thread_fn(): |
| 685 | + logging._acquireLock() |
| 686 | + try: |
| 687 | + refed_h.acquire() |
| 688 | + try: |
| 689 | + # Tell the main thread to do the fork. |
| 690 | + locks_held__ready_to_fork.set() |
| 691 | + |
| 692 | + # If the deadlock bug exists, the fork will happen |
| 693 | + # without dealing with the locks we hold, deadlocking |
| 694 | + # the child. |
| 695 | + |
| 696 | + # Wait for a successful fork or an unreasonable amount of |
| 697 | + # time before releasing our locks. To avoid a timing based |
| 698 | + # test we'd need communication from os.fork() as to when it |
| 699 | + # has actually happened. Given this is a regression test |
| 700 | + # for a fixed issue, potentially less reliably detecting |
| 701 | + # regression via timing is acceptable for simplicity. |
| 702 | + # The test will always take at least this long. :( |
| 703 | + fork_happened__release_locks_and_end_thread.wait(0.5) |
| 704 | + finally: |
| 705 | + refed_h.release() |
| 706 | + finally: |
| 707 | + logging._releaseLock() |
| 708 | + |
| 709 | + lock_holder_thread = threading.Thread( |
| 710 | + target=lock_holder_thread_fn, |
| 711 | + name='test_post_fork_child_no_deadlock lock holder') |
| 712 | + lock_holder_thread.start() |
| 713 | + |
| 714 | + locks_held__ready_to_fork.wait() |
| 715 | + pid = os.fork() |
| 716 | + if pid == 0: # Child. |
| 717 | + logging.error(r'Child process did not deadlock. \o/') |
| 718 | + os._exit(0) |
| 719 | + else: # Parent. |
| 720 | + fork_happened__release_locks_and_end_thread.set() |
| 721 | + lock_holder_thread.join() |
| 722 | + start_time = time.monotonic() |
| 723 | + while True: |
| 724 | + waited_pid, status = os.waitpid(pid, os.WNOHANG) |
| 725 | + if waited_pid == pid: |
| 726 | + break # child process exited. |
| 727 | + if time.monotonic() - start_time > 7: |
| 728 | + break # so long? implies child deadlock. |
| 729 | + time.sleep(0.05) |
| 730 | + if waited_pid != pid: |
| 731 | + os.kill(pid, signal.SIGKILL) |
| 732 | + waited_pid, status = os.waitpid(pid, 0) |
| 733 | + self.fail("child process deadlocked.") |
| 734 | + self.assertEqual(status, 0, msg="child process error") |
| 735 | + |
669 | 736 |
|
670 | 737 | class BadStream(object):
|
671 | 738 | def write(self, data):
|
|
0 commit comments