From 4fcf78b342194b4efe473c454a65e71d1f2ebee0 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Wed, 18 Aug 2010 14:10:47 +0000 Subject: [PATCH] New Features: - Connection objects have a new peer_address attribute, which is equivilent to calling ``getpeername()`` on sockets. Bugs Fixed: - Servers using unix-domain sockets didn't clean up socket files. - When testing listeners were closed, handle_close, rather than close, was called on server connections. --- README.txt | 16 +++++ src/zc/ngi/adapters.py | 4 ++ src/zc/ngi/async.py | 7 ++ src/zc/ngi/doc/index.txt | 2 + src/zc/ngi/doc/reference.txt | 6 ++ src/zc/ngi/interfaces.py | 10 +++ src/zc/ngi/old.test | 8 +-- src/zc/ngi/testing.py | 65 +++++++++++------ src/zc/ngi/testing.test | 1 - src/zc/ngi/tests.py | 133 ++++++++++++++++++++++++++++++++--- 10 files changed, 218 insertions(+), 34 deletions(-) diff --git a/README.txt b/README.txt index 5ea1433..7e06f6b 100644 --- a/README.txt +++ b/README.txt @@ -19,6 +19,22 @@ To learn more, see http://packages.python.org/zc.ngi/ Changes ******* +==================== +2.0.0a5 (2010-08-18) +==================== + +New Features: + +- Connection objects have a new peer_address attribute, which is + equivilent to calling ``getpeername()`` on sockets. + +Bugs Fixed: + +- Servers using unix-domain sockets didn't clean up socket files. + +- When testing listeners were closed, handle_close, rather than close, + was called on server connections. + ==================== 2.0.0a4 (2010-07-27) ==================== diff --git a/src/zc/ngi/adapters.py b/src/zc/ngi/adapters.py index eede04e..714af10 100644 --- a/src/zc/ngi/adapters.py +++ b/src/zc/ngi/adapters.py @@ -61,6 +61,10 @@ def handle_exception(self, connection, reason): def handler(class_, func): return zc.ngi.generator.handler(func, class_) + @property + def peer_address(self): + return self.connection.peer_address + class Lines(Base): input = '' diff --git a/src/zc/ngi/async.py b/src/zc/ngi/async.py index 70298a4..773ffd3 100644 --- a/src/zc/ngi/async.py +++ b/src/zc/ngi/async.py @@ -433,6 +433,9 @@ def writelines(self, data): def close(self): self._dispatcher.close_after_write() + @property + def peer_address(self): + return self._dispatcher.socket.getpeername() class _ServerConnection(_Connection): zc.ngi.interfaces.implements(zc.ngi.interfaces.IServerConnection) @@ -569,6 +572,7 @@ def __init__(self, addr, handler, implementation, thready): self.__close_handler = None self._thready = thready self.__connections = set() + self.address = addr BaseListener.__init__(self, implementation) if isinstance(addr, str): family = socket.AF_UNIX @@ -658,6 +662,9 @@ def closed(self, connection): def _close(self, handler): BaseListener.close(self) + if isinstance(self.address, str) and os.path.exists(self.address): + os.remove(self.address) + if handler is None: for c in list(self.__connections): c._dispatcher.handle_close("stopped") diff --git a/src/zc/ngi/doc/index.txt b/src/zc/ngi/doc/index.txt index eb57b44..f26dff7 100644 --- a/src/zc/ngi/doc/index.txt +++ b/src/zc/ngi/doc/index.txt @@ -900,6 +900,7 @@ word-count server:: -> '2 3\n' >>> listener.close() + -> CLOSE We can also use adapters with generator-based handlers by passing an adapter factory to ``zc.ngi.generator.handler`` using the @@ -923,6 +924,7 @@ of the word count server using an adapter:: >>> connection.write('\nhello out\nthere') -> '2 3\n' >>> listener.close() + -> CLOSE By separating the low-level protocol handling from the application logic, we can reuse the low-level protocol in other applications, and diff --git a/src/zc/ngi/doc/reference.txt b/src/zc/ngi/doc/reference.txt index e11ed65..78842cb 100644 --- a/src/zc/ngi/doc/reference.txt +++ b/src/zc/ngi/doc/reference.txt @@ -31,6 +31,12 @@ by applications. .. autoclass:: IConnection :members: + .. attribute:: peer_address + + For server connections, the address of the client. For clients, + the server address. For socket-based implementations, this is + the result of calling ``getpeername()`` on the socket. + .. autoclass:: IImplementation :members: diff --git a/src/zc/ngi/interfaces.py b/src/zc/ngi/interfaces.py index ee91d4e..742359d 100644 --- a/src/zc/ngi/interfaces.py +++ b/src/zc/ngi/interfaces.py @@ -127,6 +127,16 @@ def close(): any time. """ + peer_address = Attribute( + """The peer address + + For socket-based connectionss, this is the result of calling + getpeername on the socket. + + This is primarily interesting for servers that want to vary + behavior depending on where clients connect from. + """) + class IServerConnection(IConnection): """Server connection diff --git a/src/zc/ngi/old.test b/src/zc/ngi/old.test index a6f0201..b5fe8c7 100644 --- a/src/zc/ngi/old.test +++ b/src/zc/ngi/old.test @@ -322,8 +322,8 @@ then all server connections are closed immediately and no more connections are accepted: >>> listener.close() - server closed: stopped - server closed: stopped + -> CLOSE + -> CLOSE >>> connection = zc.ngi.testing.Connection() >>> listener.connect(connection) @@ -420,11 +420,11 @@ connect method that can be used to create connections to a server. Let's connect our echo server and client. First, we'll create our server using the listener constructor: - >>> listener = zc.ngi.testing.listener(EchoServer) + >>> listener = zc.ngi.testing.listener(('localhost', 42), EchoServer) Then we'll use the connect method on the listener: - >>> client = EchoClient(listener.connect) + >>> client = EchoClient(zc.ngi.testing.connect) >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?']) server connected server got input: 'hello\n' diff --git a/src/zc/ngi/testing.py b/src/zc/ngi/testing.py index 4bd1744..9084ebc 100644 --- a/src/zc/ngi/testing.py +++ b/src/zc/ngi/testing.py @@ -48,13 +48,15 @@ class Connection: zc.ngi.interfaces.implements(zc.ngi.interfaces.IConnection) - def __init__(self, peer=None, handler=PrintingHandler): + def __init__(self, peer=None, handler=PrintingHandler, + address=None, peer_address=None): self.handler = None + self.address = address self.handler_queue = [] self.control = None self.closed = None if peer is None: - peer = Connection(self) + peer = Connection(self, address=peer_address) handler(peer) self.peer = peer @@ -74,15 +76,13 @@ def _callHandler(self, method, arg): if self.queue is None: self.queue = queue = [(method, arg)] - while queue: + while queue and not self.closed: method, arg = queue.pop(0) if method == 'handle_close': if self.control is not None: self.control.closed(self) self.closed = arg - elif self.closed: - break try: try: @@ -102,7 +102,7 @@ def _callHandler(self, method, arg): raise handler(self, arg) - except: + except Exception, v: print "Error test connection calling connection handler:" traceback.print_exc(file=sys.stdout) if method != 'handle_close': @@ -114,6 +114,8 @@ def _callHandler(self, method, arg): self.queue.append((method, arg)) def close(self): + if self.closed: + return self.peer.test_close('closed') if self.control is not None: self.control.closed(self) @@ -161,9 +163,17 @@ def writelines(self, data): except Exception, v: self._exception(v) + @property + def peer_address(self): + return self.peer.address + class _ServerConnection(Connection): zc.ngi.interfaces.implements(zc.ngi.interfaces.IServerConnection) + def __init__(self): + Connection.__init__(self, False) # False to avoid setting peer handler + self.peer = Connection(self) + class TextPrintingHandler(PrintingHandler): def handle_input(self, connection, data): @@ -174,13 +184,17 @@ def TextConnection(peer=None, handler=TextPrintingHandler): _connectable = {} _recursing = object() -def connect(addr, handler): +def connect(addr, handler, client_address=None): connections = _connectable.get(addr) if isinstance(connections, list): if connections: - return handler.connected(connections.pop(0)) + connection = connections.pop(0) + connection.peer.address = addr + connection.address = client_address + return handler.connected(connection) elif isinstance(connections, listener): - return connections.connect(addr, handler=handler) + return connections.connect(handler=handler, + client_address=client_address) elif connections is _recursing: print ( "For address, %r, a connect handler called connect from a\n" @@ -201,6 +215,7 @@ def connect(addr, handler): def connectable(addr, connection): _connectable.setdefault(addr, []).append(connection) + class listener: zc.ngi.interfaces.implements(zc.ngi.interfaces.IListener) @@ -215,22 +230,30 @@ def __init__(self, addr, handler=None): self._close_handler = None self._connections = [] - def connect(self, connection=None, handler=None): - if handler is not None: - # connection is addr in this case and is ignored - handler.connected(Connection(None, self._handler)) - return + def connect(self, connection=None, handler=None, client_address=None): if self._handler is None: raise TypeError("Listener closed") - if connection is None: + + try: # Round about None (or legacy addr) test + connection.write + orig = connection + except AttributeError: connection = _ServerConnection() - peer = connection.peer - else: - peer = None - self._connections.append(connection) + orig = None + connection.control = self + connection.peer.address = client_address + connection.address = self.address + self._connections.append(connection) self._handler(connection) - return peer + + if handler is not None: + handler.connected(connection.peer) + elif orig is None: + PrintingHandler(connection.peer) + return connection.peer + + return None connector = connect @@ -243,7 +266,7 @@ def close(self, handler=None): self._handler = None if handler is None: while self._connections: - self._connections[0].test_close('stopped') + self._connections[0].close() elif not self._connections: handler(self) else: diff --git a/src/zc/ngi/testing.test b/src/zc/ngi/testing.test index af1d617..799396b 100644 --- a/src/zc/ngi/testing.test +++ b/src/zc/ngi/testing.test @@ -45,7 +45,6 @@ simple example: client i got: Hi server h got: Hi client h got: Bye - client closed b closed If an exeption is raised by a handler, then the exception is logged and the connection is closed (if not already closed). diff --git a/src/zc/ngi/tests.py b/src/zc/ngi/tests.py index 4b7ad17..82a26c3 100644 --- a/src/zc/ngi/tests.py +++ b/src/zc/ngi/tests.py @@ -12,12 +12,15 @@ # ############################################################################## from __future__ import with_statement +from zope.testing import setupstack import doctest import logging import manuel.capture import manuel.doctest import manuel.testing +import os +import socket import sys import threading import time @@ -601,6 +604,116 @@ def testing_connection_processes_close_and_input_before_set_handler_in_order(): """ +def async_peer_address(): + r""" + >>> @zc.ngi.adapters.Lines.handler + ... def server(connection): + ... host, port = connection.peer_address + ... if not (host == '127.0.0.1' and isinstance(port, int)): + ... print 'oops', host, port + ... data = (yield) + ... connection.write(data+'\n') + ... listener.close() + + >>> listener = zc.ngi.async.listener(None, server) + + >>> @zc.ngi.adapters.Lines.handler + ... def client(connection): + ... connection.write('hi\n') + ... yield + + >>> zc.ngi.async.connect(listener.address, client); zc.ngi.async.wait(1) + + """ + +def testing_peer_address(): + r""" + >>> @zc.ngi.adapters.Lines.handler + ... def server(connection): + ... print `connection.peer_address` + ... data = (yield) + ... connection.write(data+'\n') + ... listener.close() + + >>> listener = zc.ngi.testing.listener('', server) + + >>> @zc.ngi.adapters.Lines.handler + ... def client(connection): + ... connection.write('hi\n') + ... yield + + >>> zc.ngi.testing.connect(listener.address, client, + ... client_address=('xxx', 0)) + ('xxx', 0) + + Obscure: + + >>> conn = zc.ngi.testing.Connection(address='1', peer_address='2') + >>> conn.peer_address, conn.peer.peer_address + ('2', '1') + + >>> conn = zc.ngi.testing.Connection() + >>> zc.ngi.testing.connectable('x', conn) + >>> zc.ngi.testing.connect('x', client, client_address='y') + -> 'hi\n' + + >>> conn.peer_address, conn.peer.peer_address + ('x', 'y') + + """ + +def async_close_unix(): + """ + +When we create and the close a unix-domain socket, we remove the +socket file so we can reopen it later. + + >>> os.listdir('.') + [] + + >>> listener = zc.ngi.async.listener('socket', lambda c: None) + >>> os.listdir('.') + ['socket'] + + >>> listener.close(); zc.ngi.async.wait(1) + >>> os.listdir('.') + [] + + >>> listener = zc.ngi.async.listener('socket', lambda c: None) + >>> os.listdir('.') + ['socket'] + + >>> listener.close(); zc.ngi.async.wait(1) + >>> os.listdir('.') + [] + + """ + +def async_peer_address_unix(): + r""" + >>> @zc.ngi.adapters.Lines.handler + ... def server(connection): + ... print `connection.peer_address` + ... data = (yield) + ... connection.write(data+'\n') + ... listener.close() + + >>> listener = zc.ngi.async.listener('sock', server) + + >>> @zc.ngi.adapters.Lines.handler + ... def client(connection): + ... connection.write('hi\n') + ... yield + + >>> zc.ngi.async.connect(listener.address, client); zc.ngi.async.wait(1) + '' + + """ + +if not hasattr(socket, 'AF_UNIX'): + # windows + del async_peer_address_unix, async_close_unix + if sys.version_info < (2, 6): del setHandler_compatibility @@ -618,16 +731,19 @@ def connected(self, connection): handle_input = handle_close = lambda: xxxxx +def setUp(test): + cleanup() + setupstack.setUpDirectory(test) + setupstack.register(test, cleanup) + def async_evil_setup(test): + setUp(test) # Uncomment the next 2 lines to check that a bunch of lambda type # errors are logged. #import logging #logging.getLogger().addHandler(logging.StreamHandler()) - # clean up the map. - zc.ngi.async.cleanup_map() - # See if we can break the main loop before running the async test # Connect to bad port with bad handler @@ -657,7 +773,8 @@ def async_evil_setup(test): zc.ngi.async.listener(addr, BrokenAfterConnect()) zc.ngi.async.connect(addr, BrokenAfterConnect()) -def cleanup_async(test): +def cleanup(): + zc.ngi.testing._connectable.clear() zc.ngi.async.cleanup_map() zc.ngi.async.wait(9) @@ -666,7 +783,7 @@ def test_suite(): manuel.testing.TestSuite( manuel.capture.Manuel() + manuel.doctest.Manuel(), 'doc/index.txt', - ), + setUp=setUp, tearDown=setupstack.tearDown), doctest.DocFileSuite( 'old.test', 'testing.test', @@ -674,12 +791,12 @@ def test_suite(): 'adapters.test', 'blocking.test', 'async-udp.test', - tearDown=cleanup_async), + setUp=setUp, tearDown=setupstack.tearDown), doctest.DocFileSuite( 'async.test', - setUp=async_evil_setup, tearDown=cleanup_async, + setUp=async_evil_setup, tearDown=setupstack.tearDown, ), - doctest.DocTestSuite(setUp=cleanup_async, tearDown=cleanup_async), + doctest.DocTestSuite(setUp=setUp, tearDown=setupstack.tearDown), ]) if __name__ == '__main__':