Index: Misc/NEWS =================================================================== --- Misc/NEWS (revision 81078) +++ Misc/NEWS (working copy) @@ -351,6 +351,9 @@ Library ------- +- Issue #7978: SocketServer doesn't handle syscall interruption; a wrapper was + added around SocketServer's use of select() to resolve that. + - Issue #4972: Add support for the context manager protocol to the ftplib.FTP class. Index: Lib/socketserver.py =================================================================== --- Lib/socketserver.py (revision 81078) +++ Lib/socketserver.py (working copy) @@ -133,6 +133,7 @@ import select import sys import os +import errno try: import threading except ImportError: @@ -147,6 +148,15 @@ "ThreadingUnixStreamServer", "ThreadingUnixDatagramServer"]) +def _eintr_retry(func, *args): + """restart a system call interrupted by EINTR""" + while True: + try: + return func(*args) + except (OSError, IOError, select.error) as e: + if e.args[0] != errno.EINTR: + raise + class BaseServer: """Base class for server classes. @@ -222,7 +232,8 @@ # connecting to the socket to wake this up instead of # polling. Polling reduces our responsiveness to a # shutdown request and wastes cpu at all other times. - r, w, e = select.select([self], [], [], poll_interval) + r, w, e = _eintr_retry(select.select, [self], [], [], + poll_interval) if self in r: self._handle_request_noblock() finally: @@ -262,7 +273,7 @@ timeout = self.timeout elif self.timeout is not None: timeout = min(timeout, self.timeout) - fd_sets = select.select([self], [], [], timeout) + fd_sets = _eintr_retry(select.select, [self], [], [], timeout) if not fd_sets[0]: self.handle_timeout() return Index: Lib/test/test_socketserver.py =================================================================== --- Lib/test/test_socketserver.py (revision 81078) +++ Lib/test/test_socketserver.py (working copy) @@ -11,6 +11,7 @@ import tempfile import unittest import socketserver +import time import test.support from test.support import reap_children, reap_threads, verbose @@ -122,6 +123,23 @@ self.assertEquals(server.server_address, server.socket.getsockname()) return server + def fork_server(self, svrcls, hdlrbase): + pid = os.fork() + if pid == 0: + # child: must never resume execution beyond this block + server = self.make_server(self.pickaddr(svrcls.address_family), + svrcls, hdlrbase) + signal.signal(signal.SIGUSR1, lambda sig, frame: None) + try: + if verbose: print("server running, pid %d" % (os.getpid(),)) + server.serve_forever() + except Exception as error: + if verbose: print("server raised unexpected %s" % (error,)) + finally: + os._exit(1) + # child: no matter what, child dies here + return pid + @unittest.skipUnless(threading, 'Threading required for this test.') @reap_threads def run_server(self, svrcls, hdlrbase, testfunc): @@ -173,6 +191,13 @@ self.assertEquals(buf, TEST_STR) s.close() + def kill_and_validate_server(self, pid): + if verbose: print("killing server at %d" % (pid,)) + os.kill(pid, signal.SIGTERM) + pid2, status = os.waitpid(pid, 0) + self.assertEquals(pid2, pid) + self.assertEquals(signal.SIGTERM, status) + def test_TCPServer(self): self.run_server(socketserver.TCPServer, socketserver.StreamRequestHandler, @@ -225,7 +250,25 @@ socketserver.DatagramRequestHandler, self.dgram_examine) - # Alas, on Linux (at least) recvfrom() doesn't return a meaningful + @unittest.skipUnless(HAVE_FORKING, "Forking required for this test.") + def test_InterruptServerSelectCall(self): + pid = self.fork_server(socketserver.TCPServer, + socketserver.StreamRequestHandler) + + # XXX give the server time to reach select(); not very elegant, + # but anything else I can think of will be overly complicated + # for a unittest + time.sleep(0.2) + os.kill(pid, signal.SIGUSR1) + # XXX give the server time to handle this SIGUSR1 (and possible die + # from an exception raised because of it) before we send the + # SIGTERM + time.sleep(0.2) + + self.kill_and_validate_server(pid) + if verbose: print("done") + + # Alas, on Linux (at least) recvfrom() doesn't return a meaningful # client address so this cannot work: # if HAVE_UNIX_SOCKETS: