Index: Lib/asyncore.py =================================================================== --- Lib/asyncore.py (revision 74273) +++ Lib/asyncore.py (working copy) @@ -70,6 +70,8 @@ pass _reraised_exceptions = (ExitNow, KeyboardInterrupt, SystemExit) +_close_socket = frozenset((ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED)) +_poll2_close_socket = _close_socket | frozenset((EBADF,)) def read(obj): try: @@ -89,7 +91,22 @@ def _exception(obj): try: - obj.handle_expt_event() + try: + err = obj.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + except AttributeError: + # This is a non-socket file handle. + # We put the close even there so that obj.handle_error() can be + # called if closing fails. + obj.handle_close() + return + # this is a standard socket with an error or oob data + if err != 0: + if not obj.connected: + # the socket wasn't connected, raise an exception + raise socket.error(err, _strerror(err)) + obj.handle_close() + else: + obj.handle_expt_event() except _reraised_exceptions: raise except: @@ -106,7 +123,7 @@ if flags & (select.POLLHUP | select.POLLERR | select.POLLNVAL): obj.handle_close() except socket.error, e: - if e.args[0] not in (EBADF, ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED): + if e.args[0] not in _poll2_close_socket: obj.handle_error() else: obj.handle_close() @@ -359,7 +376,7 @@ except socket.error, why: if why.args[0] == EWOULDBLOCK: return 0 - elif why.args[0] in (ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED): + elif why.args[0] in _close_socket: self.handle_close() return 0 else: @@ -377,7 +394,7 @@ return data except socket.error, why: # winsock sometimes throws ENOTCONN - if why.args[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED]: + if why.args[0] in _close_socket: self.handle_close() return '' else: @@ -415,6 +432,11 @@ # sockets that are connected self.handle_accept() elif not self.connected: + # check for errors + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + raise socket.error(err, _strerror(err)) + self.handle_connect_event() self.handle_read() else: @@ -431,28 +453,17 @@ return if not self.connected: - #check for errors + # check for errors err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if err != 0: raise socket.error(err, _strerror(err)) self.handle_connect_event() + self.handle_write() def handle_expt_event(self): - # handle_expt_event() is called if there might be an error on the - # socket, or if there is OOB data - # check for the error condition first - err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) - if err != 0: - # we can get here when select.select() says that there is an - # exceptional condition on the socket - # since there is an error, we'll go ahead and close the socket - # like we would in a subclassed handle_read() that received no - # data - self.handle_close() - else: - self.handle_expt() + self.handle_expt() def handle_error(self): nil, t, v, tbinfo = compact_traceback() Index: Lib/test/test_asyncore.py =================================================================== --- Lib/test/test_asyncore.py (revision 74273) +++ Lib/test/test_asyncore.py (working copy) @@ -1,8 +1,10 @@ import asyncore +import asynchat import unittest import select import os import socket +import errno import threading import sys import time @@ -40,6 +42,7 @@ handle_write_event = handle_read_event handle_close = handle_read_event handle_expt_event = handle_read_event + handle_error = handle_read_event class crashingdummy: def __init__(self): @@ -301,8 +304,59 @@ 'warning: unhandled accept event'] self.assertEquals(lines, expected) +# this stuff tests disconnection/connection refused, as well as some +# asyncore/asynchat integration +class testing_channel(asynchat.async_chat): + def __init__(self, *a, **kw): + asynchat.async_chat.__init__(self, *a, **kw) + self.exceptions = [] + self.close_count = 0 + def collect_incoming_data(self, data): + pass + def found_terminator(self): + pass + + def handle_connect(self): + self.push("connected") + + def handle_error(self): + self.exceptions.append(sys.exc_info()[1]) + self.handle_close() + + def handle_close(self): + self.close_count += 1 + self.close() + + +class ConnectionRefusedTests(unittest.TestCase): + + def setUp(self): + s = socket.socket() + self.channel = testing_channel(s) + + # Assumes that noone is listeing here + self.channel.connect(('localhost', 65535)) + + # give time for socket to connect + time.sleep(0.1) + + asyncore.loop(count=20) + + def tearDown(self): + asyncore.close_all() + + def test_single_exception(self): + self.assertEqual(len(self.channel.exceptions), 1) + + def test_connection_refused(self): + self.assertEqual(self.channel.exceptions[0].args[0], errno.ECONNREFUSED) + + def test_close_once(self): + self.assertEqual(self.channel.close_count, 1) + + class dispatcherwithsend_noread(asyncore.dispatcher_with_send): def readable(self): return False @@ -393,8 +447,8 @@ def test_main(): - tests = [HelperFunctionTests, DispatcherTests, DispatcherWithSendTests, - DispatcherWithSendTests_UsePoll] + tests = [HelperFunctionTests, DispatcherTests, ConnectionRefusedTests, + DispatcherWithSendTests, DispatcherWithSendTests_UsePoll] if hasattr(asyncore, 'file_wrapper'): tests.append(FileWrapperTest)