Issue #8844: Regular and recursive lock acquisitions can now be interrupted
by signals on platforms using pthreads.  Patch by Reid Kleckner.
diff --git a/Lib/test/test_threadsignals.py b/Lib/test/test_threadsignals.py
index cb485fe..aa63955 100644
--- a/Lib/test/test_threadsignals.py
+++ b/Lib/test/test_threadsignals.py
@@ -6,6 +6,7 @@
 import sys
 from test.support import run_unittest, import_module
 thread = import_module('_thread')
+import time
 
 if sys.platform[:3] in ('win', 'os2') or sys.platform=='riscos':
     raise unittest.SkipTest("Can't test signal on %s" % sys.platform)
@@ -34,12 +35,12 @@
     signalled_all.release()
 
 class ThreadSignals(unittest.TestCase):
-    """Test signal handling semantics of threads.
-       We spawn a thread, have the thread send two signals, and
-       wait for it to finish. Check that we got both signals
-       and that they were run by the main thread.
-    """
+
     def test_signals(self):
+        # Test signal handling semantics of threads.
+        # We spawn a thread, have the thread send two signals, and
+        # wait for it to finish. Check that we got both signals
+        # and that they were run by the main thread.
         signalled_all.acquire()
         self.spawnSignallingThread()
         signalled_all.acquire()
@@ -66,6 +67,115 @@
     def spawnSignallingThread(self):
         thread.start_new_thread(send_signals, ())
 
+    def alarm_interrupt(self, sig, frame):
+        raise KeyboardInterrupt
+
+    def test_lock_acquire_interruption(self):
+        # Mimic receiving a SIGINT (KeyboardInterrupt) with SIGALRM while stuck
+        # in a deadlock.
+        oldalrm = signal.signal(signal.SIGALRM, self.alarm_interrupt)
+        try:
+            lock = thread.allocate_lock()
+            lock.acquire()
+            signal.alarm(1)
+            self.assertRaises(KeyboardInterrupt, lock.acquire)
+        finally:
+            signal.signal(signal.SIGALRM, oldalrm)
+
+    def test_rlock_acquire_interruption(self):
+        # Mimic receiving a SIGINT (KeyboardInterrupt) with SIGALRM while stuck
+        # in a deadlock.
+        oldalrm = signal.signal(signal.SIGALRM, self.alarm_interrupt)
+        try:
+            rlock = thread.RLock()
+            # For reentrant locks, the initial acquisition must be in another
+            # thread.
+            def other_thread():
+                rlock.acquire()
+            thread.start_new_thread(other_thread, ())
+            # Wait until we can't acquire it without blocking...
+            while rlock.acquire(blocking=False):
+                rlock.release()
+                time.sleep(0.01)
+            signal.alarm(1)
+            self.assertRaises(KeyboardInterrupt, rlock.acquire)
+        finally:
+            signal.signal(signal.SIGALRM, oldalrm)
+
+    def acquire_retries_on_intr(self, lock):
+        self.sig_recvd = False
+        def my_handler(signal, frame):
+            self.sig_recvd = True
+        old_handler = signal.signal(signal.SIGUSR1, my_handler)
+        try:
+            def other_thread():
+                # Acquire the lock in a non-main thread, so this test works for
+                # RLocks.
+                lock.acquire()
+                # Wait until the main thread is blocked in the lock acquire, and
+                # then wake it up with this.
+                time.sleep(0.5)
+                os.kill(process_pid, signal.SIGUSR1)
+                # Let the main thread take the interrupt, handle it, and retry
+                # the lock acquisition.  Then we'll let it run.
+                time.sleep(0.5)
+                lock.release()
+            thread.start_new_thread(other_thread, ())
+            # Wait until we can't acquire it without blocking...
+            while lock.acquire(blocking=False):
+                lock.release()
+                time.sleep(0.01)
+            result = lock.acquire()  # Block while we receive a signal.
+            self.assertTrue(self.sig_recvd)
+            self.assertTrue(result)
+        finally:
+            signal.signal(signal.SIGUSR1, old_handler)
+
+    def test_lock_acquire_retries_on_intr(self):
+        self.acquire_retries_on_intr(thread.allocate_lock())
+
+    def test_rlock_acquire_retries_on_intr(self):
+        self.acquire_retries_on_intr(thread.RLock())
+
+    def test_interrupted_timed_acquire(self):
+        # Test to make sure we recompute lock acquisition timeouts when we
+        # receive a signal.  Check this by repeatedly interrupting a lock
+        # acquire in the main thread, and make sure that the lock acquire times
+        # out after the right amount of time.
+        self.start = None
+        self.end = None
+        self.sigs_recvd = 0
+        done = thread.allocate_lock()
+        done.acquire()
+        lock = thread.allocate_lock()
+        lock.acquire()
+        def my_handler(signum, frame):
+            self.sigs_recvd += 1
+        old_handler = signal.signal(signal.SIGUSR1, my_handler)
+        try:
+            def timed_acquire():
+                self.start = time.time()
+                lock.acquire(timeout=0.5)
+                self.end = time.time()
+            def send_signals():
+                for _ in range(40):
+                    time.sleep(0.05)
+                    os.kill(process_pid, signal.SIGUSR1)
+                done.release()
+
+            # Send the signals from the non-main thread, since the main thread
+            # is the only one that can process signals.
+            thread.start_new_thread(send_signals, ())
+            timed_acquire()
+            # Wait for thread to finish
+            done.acquire()
+            # This allows for some timing and scheduling imprecision
+            self.assertLess(self.end - self.start, 2.0)
+            self.assertGreater(self.end - self.start, 0.3)
+            self.assertEqual(40, self.sigs_recvd)
+        finally:
+            signal.signal(signal.SIGUSR1, old_handler)
+
 
 def test_main():
     global signal_blackboard