[Checkins] SVN: zc.ngi/trunk/ Initial version
Jim Fulton
jim at zope.com
Fri Aug 11 08:55:35 EDT 2006
Log message for revision 69400:
Initial version
Changed:
A zc.ngi/trunk/bootstrap.py
A zc.ngi/trunk/buildout.cfg
A zc.ngi/trunk/setup.py
A zc.ngi/trunk/src/
A zc.ngi/trunk/src/zc/
A zc.ngi/trunk/src/zc/__init__.py
A zc.ngi/trunk/src/zc/ngi/
A zc.ngi/trunk/src/zc/ngi/README.txt
A zc.ngi/trunk/src/zc/ngi/__init__.py
A zc.ngi/trunk/src/zc/ngi/adapters.py
A zc.ngi/trunk/src/zc/ngi/adapters.txt
A zc.ngi/trunk/src/zc/ngi/async.py
A zc.ngi/trunk/src/zc/ngi/async.txt
A zc.ngi/trunk/src/zc/ngi/interfaces.py
A zc.ngi/trunk/src/zc/ngi/message.py
A zc.ngi/trunk/src/zc/ngi/message.txt
A zc.ngi/trunk/src/zc/ngi/notes.txt
A zc.ngi/trunk/src/zc/ngi/testing.py
A zc.ngi/trunk/src/zc/ngi/tests.py
A zc.ngi/trunk/src/zc/ngi/wordcount.py
-=-
Added: zc.ngi/trunk/bootstrap.py
===================================================================
--- zc.ngi/trunk/bootstrap.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/bootstrap.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,48 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+ ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+ os.P_WAIT, sys.executable, sys.executable,
+ '-c', 'from setuptools.command.easy_install import main; main()',
+ '-mqNxd', tmpeggs, 'zc.buildout',
+ {'PYTHONPATH':
+ ws.find(pkg_resources.Requirement.parse('setuptools')).location
+ },
+ ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
Property changes on: zc.ngi/trunk/bootstrap.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/buildout.cfg
===================================================================
--- zc.ngi/trunk/buildout.cfg 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/buildout.cfg 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,7 @@
+[buildout]
+develop = .
+parts = test
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = ngi
Property changes on: zc.ngi/trunk/buildout.cfg
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/setup.py
===================================================================
--- zc.ngi/trunk/setup.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/setup.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,19 @@
+from setuptools import setup, find_packages
+
+name = 'ngi'
+setup(
+ name = name,
+ version = "0.1",
+ author = "Jim Fulton",
+ author_email = "jim#zope.com",
+ description = "Network Gateway Interface",
+ license = "ZPL 2.1",
+ keywords = "network",
+ url='http://svn.zope.org/ngi',
+
+ packages = find_packages('src'),
+ include_package_data = True,
+ package_dir = {'':'src'},
+ namespace_packages = ['zc'],
+ install_requires = ['setuptools', 'zope.testing'],
+ )
Property changes on: zc.ngi/trunk/setup.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/__init__.py
===================================================================
--- zc.ngi/trunk/src/zc/__init__.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/__init__.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
Property changes on: zc.ngi/trunk/src/zc/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/README.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/README.txt 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/README.txt 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,385 @@
+=========================
+Network Gateway Interface
+=========================
+
+Network programs are typically difficult to test because they require
+setting up network connections, clientts, and servers. In addition,
+application code gets mixed up with networking code.
+
+The Network Gateway Interface (NGI) seeks to improve this situation by
+separating application code from network code. This allows
+application and network code to be tested indepenndly and provides
+greater separation of concerns.
+
+There are several interfaces defined by the NGI:
+
+IConnection
+ Network connection implementation. This is the core interface that
+ applications interact with,
+
+IConnectionHandler
+ Application component that handles network input.
+
+IConnector
+ Create IConnection objects by making outgoing connections.
+
+IClientConnectHandler
+ Application callback that handles successful ot failed outgoing
+ connections.
+
+IListener
+ Listed for incoming connections.
+
+IServer
+ Callback to handle incoming connections.
+
+The interfaces are split between "implementation" and "application"
+interfaces. An implementation of the NGI provides IConnection,
+IConnector, and IListener. An application provides IConnectionHandler
+and one or both of IClientConnectHandler and IServer.
+
+For more information, see interfaces.py.
+
+Testing Implementation
+======================
+
+These interface can have a number of implementations. The simplest
+implementation is the testing implementation, which is used to test
+application code.
+
+ >>> import zc.ngi.testing
+
+The testing module provides IConnection, IConnector, and IListener
+implentations. We'll use this below to illustrate how application code
+is written.
+
+Implementing Network Clients
+============================
+
+Network clients make connections to and then use these connections to
+communicate with servers. To do so, a client must be provided with an
+IConnector implemantation. How this happens is outside the scope of
+the NGI. An IConnector implementation could, for example, be provided
+via the Zope component architecture, or via package_resources entry
+points.
+
+Let's create a simple client that calls an echo server and verifies
+that the server properly echoes data sent do it.
+
+ >>> class EchoClient:
+ ...
+ ... def __init__(self, connector):
+ ... self.connector = connector
+ ...
+ ... def check(self, addr, strings):
+ ... self.strings = strings
+ ... self.connector(addr, self)
+ ...
+ ... def connected(self, connection):
+ ... for s in self.strings:
+ ... connection.write(s + '\n')
+ ... self.input = ''
+ ... connection.setHandler(self)
+ ...
+ ... def failed_connect(self, reason):
+ ... print 'failed connect:', reason
+ ...
+ ... def handle_input(self, connection, data):
+ ... print 'got input:', repr(data)
+ ... self.input += data
+ ... while '\n' in self.input:
+ ... data, self.input = self.input.split('\n', 1)
+ ... if self.strings:
+ ... expected = self.strings.pop(0)
+ ... if data == expected:
+ ... print 'matched:', data
+ ... else:
+ ... print 'unmatched:', data
+ ... if not self.strings:
+ ... connection.close()
+ ... else:
+ ... print 'Unexpected input', data
+ ...
+ ... def handle_close(self, connection, reason):
+ ... print 'closed:', reason
+ ... if self.strings:
+ ... print 'closed prematurely'
+
+
+The client impements the IClientConnectHandler and IInputHandler
+interfaces. More complex clients might implement these interfacs with
+separate classes.
+
+We'll instantiate our client using the testing connector:
+
+ >>> client = EchoClient(zc.ngi.testing.connector)
+
+Now we'll try to check a non-existent server:
+
+ >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?'])
+ failed connect: no such server
+
+Our client simply prints a message (and gives up) if a connection
+fails. More complex applications might retry, waiting between attemps,
+and so on.
+
+The testing connector always fails unless given a test connection
+ahead of time. We'll create a testing connection and register it so a
+connection can suceed:
+
+ >>> connection = zc.ngi.testing.Connection()
+ >>> zc.ngi.testing.connectable(('localhost', 42), connection)
+
+We can register multiple connections with the same address:
+
+ >>> connection2 = zc.ngi.testing.Connection()
+ >>> zc.ngi.testing.connectable(('localhost', 42), connection2)
+
+The connections will be used in order.
+
+Now, our client should be able to connect to the first connection we
+created:
+
+ >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?'])
+ -> 'hello\n'
+ -> 'world\n'
+ -> 'how are you?\n'
+
+The test connection echoes data written to it, preceeded by "-> ".
+
+Active connections are true:
+
+ >>> bool(connection2)
+ True
+
+Test connections provide mehods generating test input and flow closing
+connections. We can use these to simulate network events. Let's
+generate some input for our client:
+
+ >>> connection.test_input('hello')
+ got input: 'hello'
+
+ >>> connection.test_input('\nbob\n')
+ got input: '\nbob\n'
+ matched: hello
+ unmatched: bob
+
+ >>> connection.test_close('done')
+ closed: done
+ closed prematurely
+
+ >>> client.check(('localhost', 42), ['hello'])
+ -> 'hello\n'
+
+ >>> connection2.test_input('hello\n')
+ got input: 'hello\n'
+ matched: hello
+ -> CLOSE
+
+ >>> bool(connection2)
+ False
+
+
+Implementing network servers
+============================
+
+Implementing network servers is very similar to implementing clients,
+except that a listener, rather than a connector is used. Let's
+implement a simple echo server:
+
+
+ >>> class EchoServer:
+ ...
+ ... def __init__(self, connection):
+ ... print 'server connected'
+ ... self.input = ''
+ ... connection.setHandler(self)
+ ...
+ ... def handle_input(self, connection, data):
+ ... print 'server got input:', repr(data)
+ ... self.input += data
+ ... if '\n' in self.input:
+ ... data, self.input = self.input.split('\n', 1)
+ ... connection.write(data + '\n')
+ ... if data == 'Q':
+ ... connection.close()
+ ...
+ ... def handle_close(self, connection, reason):
+ ... print 'server closed:', reason
+
+Out EchoServer *class* provides IServer and implement IInputHandler.
+
+To use a server, we need a listener. We'll use the use the testing
+listener:
+
+ >>> listener = zc.ngi.testing.listener(EchoServer)
+
+To simulate a client connection, we create a testing connection and
+call the listener's connect method:
+
+ >>> connection = zc.ngi.testing.Connection()
+ >>> listener.connect(connection)
+ server connected
+
+ >>> connection.test_input('hello\n')
+ server got input: 'hello\n'
+ -> 'hello\n'
+
+ >>> connection.test_close('done')
+ server closed: done
+
+ >>> connection = zc.ngi.testing.Connection()
+ >>> listener.connect(connection)
+ server connected
+
+ >>> connection.test_input('hello\n')
+ server got input: 'hello\n'
+ -> 'hello\n'
+
+ >>> connection.test_input('Q\n')
+ server got input: 'Q\n'
+ -> 'Q\n'
+ -> CLOSE
+
+Note that it is an error to write to a closed connection:
+
+ >>> connection.write('Hello')
+ Traceback (most recent call last):
+ ...
+ TypeError: Connection closed
+
+
+Server Control
+--------------
+
+The object returned from a listener is a IServerControl. It provides
+access to the active connections:
+
+ >>> list(listener.connections())
+ []
+
+ >>> connection = zc.ngi.testing.Connection()
+ >>> listener.connect(connection)
+ server connected
+
+ >>> list(listener.connections()) == [connection]
+ True
+
+ >>> connection2 = zc.ngi.testing.Connection()
+ >>> listener.connect(connection2)
+ server connected
+
+ >>> len(list(listener.connections()))
+ 2
+ >>> connection in list(listener.connections())
+ True
+ >>> connection2 in list(listener.connections())
+ True
+
+Server connections have a control attribute that is the connections
+server control:
+
+ >>> connection.control is listener
+ True
+
+Server control objects provide a close method that allows a server to
+be shut down. If the close method is called without arguments, then
+then all server connections are closed immediately and no more
+connections are accepted:
+
+ >>> listener.close()
+ server closed: stopped
+ server closed: stopped
+
+ >>> connection = zc.ngi.testing.Connection()
+ >>> listener.connect(connection)
+ Traceback (most recent call last):
+ ...
+ TypeError: Listener closed
+
+If a handler function is passed, then connections aren't closed
+immediately:
+
+ >>> listener = zc.ngi.testing.listener(EchoServer)
+ >>> connection = zc.ngi.testing.Connection()
+ >>> listener.connect(connection)
+ server connected
+ >>> connection2 = zc.ngi.testing.Connection()
+ >>> listener.connect(connection2)
+ server connected
+
+ >>> def handler(control):
+ ... if control is listener:
+ ... print 'All connections closed'
+
+ >>> listener.close(handler)
+
+But no more connections are accepted:
+
+ >>> connection3 = zc.ngi.testing.Connection()
+ >>> listener.connect(connection3)
+ Traceback (most recent call last):
+ ...
+ TypeError: Listener closed
+
+And the handler will be called when all of the listener's connections
+are closed:
+
+ >>> connection.close()
+ -> CLOSE
+ >>> connection2.close()
+ -> CLOSE
+ All connections closed
+
+Long output
+===========
+
+Test requests output data written to them. If output exceeds 50
+characters in length, it is wrapped by simply breakng the repr into
+50-characters parts:
+
+ >>> connection = zc.ngi.testing.Connection()
+ >>> connection.write('hello ' * 50)
+ -> 'hello hello hello hello hello hello hello hello h
+ .> ello hello hello hello hello hello hello hello hel
+ .> lo hello hello hello hello hello hello hello hello
+ .> hello hello hello hello hello hello hello hello h
+ .> ello hello hello hello hello hello hello hello hel
+ .> lo hello hello hello hello hello hello hello hello
+ .> '
+
+END_OF_DATA
+===========
+
+Closing a connection closes it immediately, without sending any
+pending data. An alternate way to close a connection is to write
+zc.ngi.END_OF_DATA. The connection will be automatically closed when
+zc.ngi.END_OF_DATA is encountered in the output stream.
+
+ >>> connection.write(zc.ngi.END_OF_DATA)
+ -> CLOSE
+
+ >>> connection.write('Hello')
+ Traceback (most recent call last):
+ ...
+ TypeError: Connection closed
+
+Peer connectors
+===============
+
+It is sometimes useful to connect a client handler and a server
+handler. The zc.ngi.testing.peer function can be used to create a
+connection to a peer handler. To illustrate, we'll set up an echo
+client that connects to our echo server:
+
+ >>> client = EchoClient(zc.ngi.testing.peer(('localhost', 42), EchoServer))
+ >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?'])
+ server connected
+ server got input: 'hello\n'
+ server got input: 'world\n'
+ server got input: 'how are you?\n'
+ got input: 'hello\nworld\nhow are you?\n'
+ matched: hello
+ matched: world
+ matched: how are you?
+ server closed: closed
Property changes on: zc.ngi/trunk/src/zc/ngi/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/__init__.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/__init__.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/__init__.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,19 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Network Gateway Interface
+
+$Id$
+"""
+
+END_OF_DATA = object()
Property changes on: zc.ngi/trunk/src/zc/ngi/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/adapters.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/adapters.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/adapters.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,42 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""NGI connection adapters
+
+$Id$
+"""
+
+class Lines:
+
+ def __init__(self, connection):
+ self.connection = connection
+ self.close = connection.close
+ self.write = connection.write
+
+ def setHandler(self, handler):
+ self.handler = handler
+ self.input = ''
+ self.connection.setHandler(self)
+
+ def handle_input(self, connection, data):
+ self.input += data
+ data = self.input.split('\n')
+ self.input = data.pop()
+ for line in data:
+ self.handler.handle_input(self, line)
+
+ def handle_close(self, connection, reason):
+ self.handler.handle_close(self, reason)
+
+
+
Property changes on: zc.ngi/trunk/src/zc/ngi/adapters.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/adapters.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/adapters.txt 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/adapters.txt 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,62 @@
+============
+NGI Adapters
+============
+
+The NGI is a fairly low-level event-based framework. Adapters can be
+used to build higher-level sematics. In this document, we'll describe
+some sample adapters that provide more examples of using the NGI and
+useful building blocks for other applications. The sound for these
+adapters can be found in the zc.ngi.adapters module.
+
+Lines
+=====
+
+The first adapter we'll look at collects input into lines. To
+illustrate this, we'll use a handler from zc.ngi.testing that simply
+prints its input:
+
+ >>> import zc.ngi.testing
+ >>> connection = zc.ngi.testing.Connection()
+ >>> handler = zc.ngi.testing.PrintingHandler(connection)
+
+This handler is used by by default as the peer
+of testing connections:
+
+ >>> connection.test_input('x' * 80)
+ -> 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ .> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
+
+ >>> connection.test_close('test')
+ -> CLOSE test
+
+Now, we'll use the lines adapter to break input into lines, separated
+by newlines. We apply the adapter to a connection:
+
+ >>> import zc.ngi.adapters
+ >>> connection = zc.ngi.testing.Connection()
+ >>> adapter = zc.ngi.adapters.Lines(connection)
+ >>> handler = zc.ngi.testing.PrintingHandler(adapter)
+
+Now, when we provide input, it won't appear until lines are complete:
+
+ >>> connection.test_input('Hello world!')
+ >>> connection.test_input('\n')
+ -> 'Hello world!'
+
+ >>> connection.test_input('Hello\nWorld!\nHow are you')
+ -> 'Hello'
+ -> 'World!'
+
+Only input handling is affected. Other methods of the adapter behave
+as would the underlying connnection:
+
+ >>> adapter.write('foo')
+ -> 'foo'
+
+ >>> connection.test_close('test')
+ -> CLOSE test
+
+The original connection is available in the connection attribute:
+
+ >>> adapter.connection is connection
+ True
Property changes on: zc.ngi/trunk/src/zc/ngi/adapters.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/async.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/async.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/async.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,449 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Asyncore-based implementation of the NGI
+
+$Id$
+"""
+
+import asyncore
+import errno
+import logging
+import os
+import select
+import socket
+import sys
+import threading
+import time
+
+import zc.ngi
+
+pid = os.getpid()
+
+_map = {}
+
+expected_socket_read_errors = {
+ errno.EWOULDBLOCK: 0,
+ errno.EAGAIN: 0,
+ errno.EINTR: 0,
+ }
+
+expected_socket_write_errors = {
+ errno.EAGAIN: 0,
+ errno.EWOULDBLOCK: 0,
+ errno.ENOBUFS: 0,
+ errno.EINTR: 0,
+ }
+
+class dispatcher(asyncore.dispatcher):
+
+ def __init__(self, sock, addr):
+ self.addr = addr
+ asyncore.dispatcher.__init__(self, sock, _map)
+
+ def handle_error(self):
+ reason = sys.exc_info()[1]
+ self.logger.exception('handle_error')
+ self.handle_close(reason)
+ self.close()
+
+ def close(self):
+ self.del_channel(_map)
+ self.socket.close()
+
+ def writable(self):
+ return False
+
+class _Connection(dispatcher):
+
+ control = None
+
+ def __init__(self, sock, addr, logger):
+ self.__connected = True
+ self.__closed = None
+ self.__handler = None
+ self.__output = []
+ self.__handler_lock = threading.Lock()
+ dispatcher.__init__(self, sock, addr)
+ self.logger = logger
+
+ def __nonzero__(self):
+ return self.__connected
+
+ def setHandler(self, handler):
+ if self.__handler is not None:
+ raise TypeError("Handler already set")
+
+ self.__handler_lock.acquire()
+ try:
+ self.__handler = handler
+ if self.__closed:
+ handler.handle_close(seld, self.__closed)
+ finally:
+ self.__handler_lock.release()
+
+ def write(self, data):
+ if __debug__:
+ self.logger.debug('write %r', data)
+ self.__output.append(data)
+ notify_select()
+
+ def close(self):
+ self.__connected = False
+ dispatcher.close(self)
+ if self.control is not None:
+ self.control.closed(self)
+ notify_select()
+
+ def readable(self):
+ return self.__handler is not None
+
+ def writable(self):
+ return bool(self.__output)
+
+ def handle_read_event(self):
+ assert self.readable()
+
+ while 1:
+ try:
+ d = self.recv(8192)
+ except socket.error, err:
+ if err[0] in expected_socket_read_errors:
+ return
+ raise
+
+ if not d:
+ return
+
+ if __debug__:
+ self.logger.debug('input %r', d)
+ self.__handler.handle_input(self, d)
+ if len(d) < 8192:
+ break
+
+ def handle_write_event(self):
+ if __debug__:
+ self.logger.debug('handle_write_event')
+
+ while self.__output:
+ output = self.__output
+ v = output[0]
+ if v == zc.ngi.END_OF_DATA:
+ self.close()
+ return
+
+ try:
+ n = self.send(v)
+ except socket.error, err:
+ if err[0] in expected_socket_write_errors:
+ return # we couldn't write anything
+ raise
+
+ if n == len(v):
+ output.pop(0)
+ else:
+ output[0] = v[n:]
+ return # can't send any more
+
+ def handle_close(self, reason='end of input'):
+ if __debug__:
+ self.logger.debug('close %r', reason)
+ self.__handler_lock.acquire()
+ try:
+ if self.__handler is not None:
+ self.__handler.handle_close(self, reason)
+ else:
+ self.__closed = reason
+ self.close()
+ finally:
+ self.__handler_lock.release()
+
+ def handle_expt(self):
+ self.handle_close('expt')
+
+
+class connector(dispatcher):
+
+ logger = logging.getLogger('zc.ngi.async.client')
+
+ # When trying to do a connect on a non-blocking socket, some outcomes
+ # are expected. Set _CONNECT_IN_PROGRESS to the errno value(s) expected
+ # when an initial connect can't complete immediately. Set _CONNECT_OK
+ # to the errno value(s) expected if the connect succeeds *or* if it's
+ # already connected (our code can attempt redundant connects).
+ if hasattr(errno, "WSAEWOULDBLOCK"): # Windows
+ # Caution: The official Winsock docs claim that WSAEALREADY should be
+ # treated as yet another "in progress" indicator, but we've never
+ # seen this.
+ _CONNECT_IN_PROGRESS = (errno.WSAEWOULDBLOCK,)
+ # Win98: WSAEISCONN; Win2K: WSAEINVAL
+ _CONNECT_OK = (0, errno.WSAEISCONN, errno.WSAEINVAL)
+ else: # Unix
+ _CONNECT_IN_PROGRESS = (errno.EINPROGRESS,)
+ _CONNECT_OK = (0, errno.EISCONN)
+
+ def __init__(self, addr, handler):
+ self.__handler = handler
+ if isinstance(addr, str):
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ else:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ if __debug__:
+ self.logger.debug('connecting to %s', addr)
+ dispatcher.__init__(self, sock, addr)
+ try:
+ if self.handle_write_event():
+ return
+ except:
+ self.handle_error()
+
+ notify_select()
+
+ def readable(self):
+ return False
+
+ def writable(self):
+ return True
+
+ def handle_close(reason):
+ if __debug__:
+ self.logger.debug('connector close %r', reason)
+ self.__handler.failed_connect(reason)
+ self.close()
+
+ def handle_write_event(self):
+ err = self.socket.connect_ex(self.addr)
+ if err in self._CONNECT_IN_PROGRESS:
+ return
+
+ if err not in self._CONNECT_OK:
+ reason = errno.errorcode.get(err) or str(err)
+ self.logger.warning("error connecting to %s: %s", self.addr, reason)
+ self.handle_close(reason)
+ return
+
+ self.del_channel(_map)
+ if __debug__:
+ self.logger.debug('outgoing connected %r', self.addr)
+ self.__handler.connected(
+ _Connection(self.socket, self.addr, self.logger))
+ return
+
+class listener(asyncore.dispatcher):
+
+ logger = logging.getLogger('zc.ngi.async.server')
+
+ def __init__(self, addr, handler):
+ self.addr = addr
+ self.__handler = handler
+ self.__close_handler = None
+ self.__connections = {}
+ asyncore.dispatcher.__init__(self)
+ if isinstance(addr, str):
+ self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ else:
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.set_reuse_addr()
+ self.logger.info("listening on %s", self.addr)
+ self.bind(self.addr)
+ self.listen(5)
+ notify_select()
+
+ def handle_accept(self):
+ if not self.accepting:
+ return
+
+ try:
+ sock, addr = self.accept()
+ except socket.error, msg:
+ self.logger.exception("accepted failed: %s", msg)
+ return
+ if __debug__:
+ self.logger.debug('incoming connection %r', addr)
+
+ connection = _Connection(sock, addr, self.logger)
+ self.__connections[connection] = 1
+ connection.control = self
+ self.__handler(connection)
+
+ def connections(self):
+ return iter(self.__connections)
+
+ def closed(self, connection):
+ if connection in self.__connections:
+ del self.__connections[connection]
+ if not self.__connections and self.__close_handler:
+ self.__close_handler(self)
+
+ def close(self, handler=None):
+ self.accepting = False
+ self.del_channel(_map)
+ self.socket.close()
+ if handler is None:
+ for c in list(self._connections):
+ c.handle_close("stopped")
+ elif not self.__connections:
+ handler(self)
+ else:
+ self.__close_handler = handler
+
+ def add_channel(self):
+ # work around file-dispatcher bug
+ asyncore.dispatcher.add_channel(self, _map)
+
+ def handle_error(self):
+ reason = sys.exc_info()[1]
+ self.logger.exception('listener error')
+ self.close()
+
+# The following trigger code is greatly simplified from the Medusa
+# trigger code.
+
+class _Triggerbase(object):
+ """OS-independent base class for OS-dependent trigger class."""
+
+ logger = logging.getLogger('zc.ngi.async.trigger')
+
+ def writable(self):
+ return 0
+
+ def handle_close(self):
+ self.close()
+
+ def handle_error(self):
+ self.logger.exception('trigger error %s', pid)
+ self.close()
+
+ def handle_read(self):
+ try:
+ self.recv(8192)
+ except socket.error:
+ return
+
+if os.name == 'posix':
+
+ class _Trigger(_Triggerbase, asyncore.file_dispatcher):
+ def __init__(self):
+ self.__readfd, self.__writefd = os.pipe()
+ asyncore.file_dispatcher.__init__(self, self.__readfd)
+
+ def close(self):
+ self.del_channel(_map)
+ os.close(self.__writefd)
+ os.close(self.__readfd)
+
+ def pull_trigger(self):
+ if __debug__:
+ self.logger.debug('pulled %s', pid)
+ os.write(self.__writefd, 'x')
+
+ def add_channel(self):
+ # work around file-dispatcher bug
+ asyncore.dispatcher.add_channel(self, _map)
+
+else:
+ # Windows version; uses just sockets, because a pipe isn't select'able
+ # on Windows.
+
+ class _Trigger(_Triggerbase, asyncore.dispatcher):
+ def __init__(self):
+
+ # Get a pair of connected sockets. The trigger is the 'w'
+ # end of the pair, which is connected to 'r'. 'r' is put
+ # in the asyncore socket map. "pulling the trigger" then
+ # means writing something on w, which will wake up r.
+
+ w = socket.socket()
+ # Disable buffering -- pulling the trigger sends 1 byte,
+ # and we want that sent immediately, to wake up asyncore's
+ # select() ASAP.
+ w.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+ count = 0
+ while 1:
+ count += 1
+ # Bind to a local port; for efficiency, let the OS pick
+ # a free port for us.
+ # Unfortunately, stress tests showed that we may not
+ # be able to connect to that port ("Address already in
+ # use") despite that the OS picked it. This appears
+ # to be a race bug in the Windows socket implementation.
+ # So we loop until a connect() succeeds (almost always
+ # on the first try). See the long thread at
+ # http://mail.zope.org/pipermail/zope/2005-July/160433.html
+ # for hideous details.
+ a = socket.socket()
+ a.bind(("127.0.0.1", 0))
+ connect_address = a.getsockname() # assigned (host, port) pair
+ a.listen(1)
+ try:
+ w.connect(connect_address)
+ break # success
+ except socket.error, detail:
+ if detail[0] != errno.WSAEADDRINUSE:
+ # "Address already in use" is the only error
+ # I've seen on two WinXP Pro SP2 boxes, under
+ # Pythons 2.3.5 and 2.4.1.
+ raise
+ # (10048, 'Address already in use')
+ # assert count <= 2 # never triggered in Tim's tests
+ if count >= 10: # I've never seen it go above 2
+ a.close()
+ w.close()
+ raise BindError("Cannot bind trigger!")
+ # Close `a` and try again. Note: I originally put a short
+ # sleep() here, but it didn't appear to help or hurt.
+ a.close()
+
+ r, addr = a.accept() # r becomes asyncore's (self.)socket
+ a.close()
+ self.trigger = w
+ asyncore.dispatcher.__init__(self, r, _map)
+
+ def close(self):
+ self.del_channel(_map)
+ # self.socket is r, and self.trigger is w, from __init__
+ self.socket.close()
+ self.trigger.close()
+
+ def pull_trigger(self):
+ if __debug__:
+ self.logger.debug('notify select %s', pid)
+ self.trigger.send('x')
+
+_trigger = _Trigger()
+
+notify_select = _trigger.pull_trigger
+
+def loop():
+ timeout = 30.0
+ map = _map
+ if hasattr(select, 'poll'):
+ poll_fun = asyncore.poll3
+ else:
+ poll_fun = asyncore.poll
+
+ logger = logging.getLogger('zc.ngi.async.loop')
+
+ while map:
+ try:
+ poll_fun(timeout, map)
+ except:
+ logger.exception('loop error')
+ raise
+
+_thread = threading.Thread(target=loop)
+_thread.setDaemon(True)
+_thread.start()
+
Property changes on: zc.ngi/trunk/src/zc/ngi/async.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/async.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/async.txt 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/async.txt 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,50 @@
+=================================
+asyncore-based NGI implementation
+=================================
+
+The async module provides an NGI implementation based on the Python
+standard asyncore framework. It provides 2 objects to be invoked
+directly by applications:
+
+connector
+ an implementation of the NGI IConnector interface
+
+listener
+ an implementation of the NGI IListener interface
+
+The implementation creates a dedicated thread to run an asyncore main
+loop on import.
+
+There's nothing else to say about the implementation from a usage
+point of view. The remainder of this document provides a
+demonstration (test) of using the impemantation to create a simple
+word-count server and client.
+
+Demonstration: wordcount
+========================
+
+The wordcount module has a simple word-count server and client
+implementation. We'll run these using the async implementation.
+
+Let's start the wordcount server:
+
+ >>> import zc.ngi.wordcount
+ >>> import zc.ngi.async
+ >>> port = zc.ngi.wordcount.start_server_process(zc.ngi.async.listener)
+
+We passed the module and name of the listener to be used.
+
+Now, we'll start a number of threads that connect to the server and
+check word counts of some sample documents. If all goes well, we
+shouldn't get any output.
+
+ >>> import threading
+ >>> addr = 'localhost', port
+ >>> threads = [threading.Thread(target=zc.ngi.wordcount.client_thread,
+ ... args=(zc.ngi.async.connector, addr))
+ ... for i in range(200)]
+
+ >>> _ = [thread.start() for thread in threads]
+ >>> _ = [thread.join() for thread in threads]
+
+ >>> zc.ngi.wordcount.stop_server_process(zc.ngi.async.connector, addr)
Property changes on: zc.ngi/trunk/src/zc/ngi/async.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/interfaces.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/interfaces.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/interfaces.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,190 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Network Gateway Interface (NGI)
+
+The interfaces are split between "implementation" and "application"
+interfaces. An implementation of the NGI provides IConnection,
+IConnector, and IListener. An application provides IConnectionHandler
+and one or both of IClientConnectHandler and IServer.
+
+The NGI is an event-based framework in the sense that applications
+register handlers that respond to input events. There are 3 kinds of
+handlers:
+
+- Input handlers recieve network input
+
+- Client-connect handlers respond to outbound connection events, and
+
+- Servers respond to incoming connection events.
+
+The interfaces are designed to allow single-threaded applications:
+
+- An implementation of the interfaces is not allowed to make multiple
+ simultanious calls to the same application handler. (Note that this
+ requirement does not extend accross multiple implementations.
+ Theoretically, different implementations could call handlers at the
+ same time.
+
+- All handler calls that are associated with a connection include the
+ connection as a parameter, This allows a single handler object to
+ respond to events from multiple connections.
+
+Applications may be multi-threaded. This means that implementations
+must be thread safe. This means that calls into the implementation
+could be made at any time.
+
+$Id$
+"""
+
+from zope.interface import Interface, Attribute
+
+class IConnection(Interface):
+ """Network connections
+
+ Network connections support communication over a network
+ connection, or any connection having separate input and output
+ channels.
+ """
+
+ def __nonzero__():
+ """Return the connection status
+
+ True is returned if the connection is open/active and
+ False otherwise.
+ """
+
+ def setHandler(handler):
+ """Set the IConnectionHandler for a connection.
+
+ The handler can be set only once.
+ """
+
+ def write(data):
+ """Write output data to a connection.
+
+ The write call is non-blocking.
+ """
+
+ def close():
+ """Close the connection
+ """
+
+class IServerConnection(IConnection):
+
+ control = Attribute("An IServerControl")
+
+class IInputHandler(Interface):
+ """Objects that can handle connection input-data events
+
+ The methods defined be this interface will never be called
+ simultaniously from separate threads, so implementation of the
+ methods needn't be concerned with thread safety with respect to
+ these methods.
+ """
+
+ def handle_input(connection, data):
+ """Handle input data from a connection
+
+ The data is an 8-bit string.
+
+ Note that there are no promises about blocking. There data
+ isn't necessarily record oriented. For example, data could,
+ in theory be passed one character at a time. It os up to
+ applications to organize data into records, if desired.
+
+ """
+
+ def handle_close(connection, reason):
+ """Recieve notification that a connection has closed
+
+ The reason argument can be converted to a string for logging
+ purposes. It may have data useful for debugging, but this
+ is undefined.
+
+ Notifications are received when the connection is closed
+ externally, for example, when the other side of the
+ connection is closed or in case of a network failure. No
+ notification is given when the connection's close method is
+ called.
+ """
+
+class IConnector(Interface):
+ """Create a connection to a server
+ """
+
+ def __call__(address, handler):
+ """Try to make a connection to the given address
+
+ The handler is an IClientConnectHandler. The handler
+ connected method will be called with an IConnection object
+ if and when the connection suceeds or failed_connect method
+ will be called if the connection fails.
+ """
+
+class IClientConnectHandler(Interface):
+ """Recieve notifications of connection results
+ """
+
+ def connected(connection):
+ """Recieve notification that a connection had been established
+ """
+
+ def failed_connect(reason):
+ """Recieve notificantion that a connection could not be established
+
+ The reason argument can be converted to a string for logging
+ purposes. It may have data useful for debugging, but this
+ is undefined.
+ """
+
+class IListener(Interface):
+ """Listed for incoming connections
+ """
+
+ def __call__(address, handler):
+ """Listen for incoming connections
+
+ When a connection is recieved, call the handler.
+
+ An IServerControl object is returned.
+ """
+
+class IServer(Interface):
+ """Handle server connections
+ """
+
+ def __call__(connection):
+ """Handle a connection from a client
+ """
+
+class IServerControl(Interface):
+ """Server information and close control
+ """
+
+ def connections():
+ """return an iterable of the current connections
+ """
+
+ def close(handler=None):
+ """Close the listener and all of it's connections
+
+ If no handler is passed, the listener and it's connections
+ are closed immediately without waiting for any pending input
+ to be handled or for pending output to be sent.
+
+ If a handler is passed, the listener will stop accepting new
+ connections and existing connections will be left open. The
+ handler will be called when all of the existing connections
+ have been closed.
+ """
Property changes on: zc.ngi/trunk/src/zc/ngi/interfaces.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/message.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/message.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/message.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,76 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample client that sends a single message and waits for a reply
+
+$Id$
+"""
+
+import threading
+
+class CouldNotConnect(Exception):
+ """Could not connect to a server
+ """
+
+class UnexpectedResponse(Exception):
+ """Got an unexpected response from a server
+ """
+
+class Message:
+
+ def __init__(self, message, expected, notify):
+ self.message = message
+ self.expected = expected
+ self.notify = notify
+ self.input = ''
+
+ def connected(self, connection):
+ connection.setHandler(self)
+ connection.write(self.message)
+
+ def failed_connect(self, reason):
+ self.notify(None, reason)
+
+ def handle_input(self, connection, data):
+ self.input += data
+ if self.expected is not None and self.expected(self.input):
+ connection.close()
+ self.handle_close(connection)
+
+ def handle_close(self, connection, reason=None):
+ self.notify(self.input, reason)
+
+
+def message(connector, addr, message, expected=None):
+ result = []
+ lock = threading.Lock()
+ lock.acquire()
+ def notify(*args):
+ if result:
+ return # already notified
+ result.extend(args)
+ lock.release()
+ connector(addr, Message(message, expected, notify))
+ lock.acquire()
+ data, reason = result
+
+ if reason is None:
+ return data
+
+ if data is None:
+ raise CouldNotConnect(reason)
+
+ if expected is not None and not expected(data):
+ raise UnexpectedResponse(data, reason)
+
+ return data
Property changes on: zc.ngi/trunk/src/zc/ngi/message.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/message.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/message.txt 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/message.txt 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,41 @@
+==============
+Message Client
+==============
+
+The message client is a simple NGI client that sends a single message
+and waits for a response. To illustrate, we'll use a simple echo
+server:
+
+ >>> class EchoServer:
+ ...
+ ... def __init__(self, connection):
+ ... self.input = ''
+ ... connection.setHandler(self)
+ ...
+ ... def handle_input(self, connection, data):
+ ... self.input += data
+ ... if '\n' in self.input:
+ ... data, self.input = self.input.split('\n', 1)
+ ... connection.write(data.upper() + '\n')
+ ...
+ ... def handle_close(self, connection, reason):
+ ... pass
+
+ >>> import zc.ngi.testing
+ >>> connector = zc.ngi.testing.peer('foo', EchoServer)
+
+and we'll use the message client to send it a message and get a
+response.
+
+ >>> import zc.ngi.message
+ >>> import re
+ >>> expected = re.compile('\n').search
+ >>> zc.ngi.message.message(connector, 'foo', 'hello world!\n', expected)
+ 'HELLO WORLD!\n'
+
+If we give an invalid address, we'll get an exception:
+
+ >>> zc.ngi.message.message(connector, 'bar', 'hello world!\n', expected)
+ Traceback (most recent call last):
+ ...
+ CouldNotConnect: connection refused
Property changes on: zc.ngi/trunk/src/zc/ngi/message.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/notes.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/notes.txt 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/notes.txt 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,11 @@
+=====
+Notes
+=====
+
+Timeout
+=======
+
+It's tempting to try to add a timeout feature, however, this adds
+quite a bit of complication, or at least time calls to get this right.
+I think it makes more sense for applications to use threads to create
+alarms.
Property changes on: zc.ngi/trunk/src/zc/ngi/notes.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/testing.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/testing.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/testing.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,149 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Testing NGI implementation
+
+$Id$
+"""
+
+import zc.ngi
+
+class PrintingHandler:
+
+ def __init__(self, connection):
+ connection.setHandler(self)
+
+ def handle_input(self, connection, data):
+ data = repr(data)
+ print '->', data[:50]
+ data = data[50:]
+ while data:
+ print '.>', data[:50]
+ data = data[50:]
+
+ def handle_close(self, connection, reason):
+ if reason != 'closed':
+ print '-> CLOSE', reason
+ else:
+ print '-> CLOSE'
+
+class Connection:
+
+ control = None
+
+ def __init__(self, peer=None, handler=PrintingHandler):
+ self.handler = None
+ self.closed = False
+ self.input = ''
+ if peer is None:
+ peer = Connection(self)
+ handler(peer)
+ self.peer = peer
+
+ def __nonzero__(self):
+ return not self.closed
+
+ def close(self):
+ self.peer.test_close('closed')
+ if self.control is not None:
+ self.control.closed(self)
+ self.closed = True
+ def write(s):
+ raise TypeError("Connection closed")
+ self.write = write
+
+ def setHandler(self, handler):
+ self.handler = handler
+ if self.input:
+ handler.handle_input(self, self.input)
+ self.input = ''
+
+ # Note is self.closed is True, we self closed and we
+ # don't want to call handle_close.
+ if self.closed and isinstance(self.closed, str):
+ handler.handle_close(self, self.closed)
+
+ def test_input(self, data):
+ if self.handler is not None:
+ self.handler.handle_input(self, data)
+ else:
+ self.input += data
+
+ def test_close(self, reason):
+ if self.control is not None:
+ self.control.closed(self)
+ self.closed = reason
+ if self.handler is not None:
+ self.handler.handle_close(self, reason)
+
+ def write(self, data):
+ if data is zc.ngi.END_OF_DATA:
+ return self.close()
+ self.peer.test_input(data)
+
+_connectable = {}
+
+def connector(addr, handler):
+ connections = _connectable.get(addr)
+ if connections:
+ handler.connected(connections.pop(0))
+ else:
+ handler.failed_connect('no such server')
+
+def connectable(addr, connection):
+ _connectable.setdefault(addr, []).append(connection)
+
+class listener:
+
+ def __init__(self, handler):
+ self._handler = handler
+ self._close_handler = None
+ self._connections = []
+
+ def connect(self, connection):
+ if self._handler is None:
+ raise TypeError("Listener closed")
+ self._connections.append(connection)
+ connection.control = self
+ self._handler(connection)
+
+ def connections(self):
+ return iter(self._connections)
+
+ def close(self, handler=None):
+ self._handler = None
+ if handler is None:
+ while self._connections:
+ self._connections[0].test_close('stopped')
+ elif not self._connections:
+ handler(self)
+ else:
+ self._close_handler = handler
+
+ def closed(self, connection):
+ self._connections.remove(connection)
+ if not self._connections and self._close_handler:
+ self._close_handler(self)
+
+class peer:
+
+ def __init__(self, addr, handler):
+ self.addr = addr
+ self.handler = handler
+
+ def __call__(self, addr, handler):
+ if addr != self.addr:
+ handler.failed_connect('connection refused')
+ else:
+ handler.connected(Connection(None, self.handler))
+
Property changes on: zc.ngi/trunk/src/zc/ngi/testing.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/tests.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/tests.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/tests.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,31 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XXX short summary goes here.
+
+$Id$
+"""
+import unittest
+from zope.testing import doctest
+import zc.ngi.async # start async thread before tests run
+
+def test_suite():
+ return doctest.DocFileSuite(
+ 'README.txt',
+ 'message.txt',
+ 'async.txt',
+ 'adapters.txt',
+ )
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Property changes on: zc.ngi/trunk/src/zc/ngi/tests.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.ngi/trunk/src/zc/ngi/wordcount.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/wordcount.py 2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/wordcount.py 2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,198 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample NGI server and client
+
+This file provides a sample NGI server and client that counts words.
+
+$Id$
+"""
+
+import errno
+import logging
+import os
+import random
+import socket
+import sys
+import threading
+import time
+
+import zc.ngi
+import zc.ngi.message
+
+_lock = threading.Lock()
+_lock.acquire()
+exit = _lock.release
+run = _lock.acquire
+
+logger = logging.getLogger('zc.ngi.wordcount')
+
+class Server:
+
+ def __init__(self, connection):
+ self.input = ''
+ connection.setHandler(self)
+
+ def handle_input(self, connection, data):
+ self.input += data
+ while '\0' in self.input:
+ data, self.input = self.input.split('\0', 1)
+ if data == 'Q':
+ connection.write('Q\n')
+ connection.write(zc.ngi.END_OF_DATA)
+ connection.control.close(lambda c: 1)
+ return
+ else:
+ cc = len(data)
+ lc = len(data.split('\n'))-1
+ wc = len(data.split())
+ connection.write("%s %s %s\n" % (lc, wc, cc))
+
+ def handle_close(self, connection, reason):
+ pass
+
+def serve():
+ mod, name, port = sys.argv[1:]
+ __import__(mod)
+ logger.info('serving')
+ getattr(sys.modules[mod], name)(('localhost', int(port)), Server)
+ run()
+
+def get_port():
+ """Return a port that is not in use.
+
+ Checks if a port is in use by trying to connect to it. Assumes it
+ is not in use if connect raises an exception.
+
+ Raises RuntimeError after 10 tries.
+ """
+ for i in range(10):
+ port = random.randrange(20000, 30000)
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ try:
+ s.connect(('localhost', port))
+ except socket.error:
+ # Perhaps we should check value of error too.
+ return port
+ finally:
+ s.close()
+ raise RuntimeError("Can't find port")
+
+def wait(addr, up=True):
+ for i in range(120):
+ time.sleep(0.25)
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(addr)
+ s.close()
+ if up:
+ break
+ except socket.error, e:
+ if e[0] not in (errno.ECONNREFUSED, errno.ECONNRESET):
+ raise
+ s.close()
+ if not up:
+ break
+ else:
+ if up:
+ print "Could not connect"
+ else:
+ print "Server still accepting connections"
+
+def start_server_process(listener):
+ """Start a server in a subprocess and return the port used
+ """
+ module = listener.__module__
+ name = listener.__name__
+ port = get_port()
+ env = dict(
+ os.environ,
+ PYTHONPATH=os.pathsep.join(sys.path),
+ )
+ os.spawnle(os.P_NOWAIT, sys.executable, sys.executable, __file__,
+ module, name, str(port), env)
+ addr = 'localhost', port
+ wait(addr)
+ return port
+
+def stop_server_process(connector, addr):
+ zc.ngi.message.message(connector, addr, 'Q\0', lambda s: s == 'Q\n')
+ wait(addr, up=False)
+
+sample_docs = [
+"""Hello world
+""",
+"""I give my pledge as an earthling
+to save and faithfully to defend from waste
+the natural resources of my planet
+its soils, minerals, forests, waters and wildlife.
+""",
+"""On my honor, I will do my best
+to do my duty to God and my country
+and to obey the Scout Law
+to always help others
+to keep myself physically strong, mentally awake, and morally straight.
+""",
+"""What we have here, is a failure to communicate.
+""",
+]
+
+class Client:
+
+ def __init__(self, docs=sample_docs, notify=None):
+ self.docs = list(docs)
+ self.notify = notify
+ self.input = ''
+
+ def connected(self, connection):
+ connection.write(self.docs[0]+'\0')
+ connection.setHandler(self)
+
+ def failed_connect(self, reason):
+ print 'Failed to connect:', reason
+
+ def handle_input(self, connection, data):
+ self.input += data
+ if '\n' in self.input:
+ data, self.input = self.input.split('\n', 1)
+ doc = self.docs.pop(0)
+ cc = len(doc)
+ lc = len(doc.split('\n'))-1
+ wc = len(doc.split())
+ expected = "%s %s %s" % (lc, wc, cc)
+ if data != expected:
+ print '%r != %r' % (data, expected)
+ if self.docs:
+ connection.write(self.docs[0]+'\0')
+ else:
+ connection.close()
+ if self.notify is not None:
+ self.notify()
+
+ def handle_close(self, connection, reason):
+ if self.docs:
+ print 'unexpected close', reason
+
+def client_thread(connector, addr):
+ logger.info('client started for %s', addr)
+ lock = threading.Lock()
+ lock.acquire()
+ client = Client(notify=lock.release)
+ connector(addr, client)
+ logger.info('client waiting')
+ lock.acquire() # wait till done
+ logger.info('client done')
+
+if __name__ == '__main__':
+ serve()
Property changes on: zc.ngi/trunk/src/zc/ngi/wordcount.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
More information about the Checkins
mailing list