diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index 7b7e29b96..3d3d9e88f 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -2,11 +2,15 @@ import os, re, itertools from base64 import b32decode import json +from socket import socket, AF_INET from twisted.trial import unittest from twisted.internet import defer, address from twisted.python import log from twisted.python.filepath import FilePath +from twisted.python.reflect import requireModule +from twisted.internet.endpoints import AdoptedStreamServerEndpoint +from twisted.internet.interfaces import IReactorSocket from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue from twisted.application import service @@ -23,6 +27,8 @@ from allmydata.client import create_client from allmydata.util import pollmixin, keyutil, idlib, fileutil, iputil, yamlutil import allmydata.test.common_util as testutil +fcntl = requireModule("fcntl") + class LoggingMultiService(service.MultiService): def log(self, msg, **kw): log.msg(msg, **kw) @@ -314,6 +320,64 @@ class Server(unittest.TestCase): NICKNAME = u"n\u00EDickname-%s" # LATIN SMALL LETTER I WITH ACUTE +def foolscapEndpointForPortNumber(portnum): + """ + Create an endpoint that can be passed to ``Tub.listen``. + + :param portnum: Either an integer port number indicating which TCP/IPv4 + port number the endpoint should bind or ``None`` to automatically + allocate such a port number. + + :return: A two-tuple of the integer port number allocated and a + Foolscap-compatible endpoint object. + """ + if portnum is None: + # Bury this reactor import here to minimize the chances of it having + # the effect of installing the default reactor. + from twisted.internet import reactor + if fcntl is not None and IReactorSocket.providedBy(reactor): + # On POSIX we can take this very safe approach of binding the + # actual socket to an address. Once the bind succeeds here, we're + # no longer subject to any future EADDRINUSE problems. + s = socket() + try: + s.bind(('', 0)) + portnum = s.getsockname()[1] + s.listen(1) + fd = os.dup(s.fileno()) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + flags = flags | os.O_NONBLOCK | fcntl.FD_CLOEXEC + fcntl.fcntl(fd, fcntl.F_SETFD, flags) + return ( + portnum, + AdoptedStreamServerEndpoint(reactor, fd, AF_INET), + ) + finally: + s.close() + else: + # Get a random port number and fall through. This is necessary on + # Windows where Twisted doesn't offer IReactorSocket. This + # approach is error prone for the reasons described on + # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2787 + portnum = iputil.allocate_tcp_port() + return (portnum, "tcp:%d" % (portnum,)) + +def listenOnUnused(tub, portnum=None): + """ + Start listening on an unused TCP port number with the given tub. + + :param portnum: Either an integer port number indicating which TCP/IPv4 + port number the endpoint should bind or ``None`` to automatically + allocate such a port number. + + :return: An integer indicating the TCP port number on which the tub is now + listening. + """ + portnum, endpoint = foolscapEndpointForPortNumber(portnum) + tub.listenOn(endpoint) + tub.setLocation("localhost:%d" % (portnum,)) + return portnum + class SystemTestMixin(ServiceMixin, pollmixin.PollMixin): def create_tub(self, portnum=None): @@ -323,11 +387,7 @@ class SystemTestMixin(ServiceMixin, pollmixin.PollMixin): #tub.setOption("logRemoteFailures", True) tub.setOption("expose-remote-exception-types", False) tub.setServiceParent(self.parent) - if portnum is None: - portnum = iputil.allocate_tcp_port() - tub.listenOn("tcp:%d" % portnum) - self.central_portnum = portnum - tub.setLocation("localhost:%d" % self.central_portnum) + self.central_portnum = listenOnUnused(tub, portnum) class Queue(SystemTestMixin, unittest.TestCase): def test_queue_until_connected(self): @@ -414,10 +474,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase): #tub.setOption("logRemoteFailures", True) tub.setOption("expose-remote-exception-types", False) tub.setServiceParent(self.parent) - portnum = iputil.allocate_tcp_port() - tub.listenOn("tcp:%d" % portnum) - tub.setLocation("localhost:%d" % portnum) - + listenOnUnused(tub) log.msg("creating client %d: %s" % (i, tub.getShortTubID())) c = IntroducerClient(tub, self.introducer_furl, NICKNAME % str(i), @@ -895,10 +952,7 @@ class NonV1Server(SystemTestMixin, unittest.TestCase): tub = Tub() tub.setOption("expose-remote-exception-types", False) tub.setServiceParent(self.parent) - portnum = iputil.allocate_tcp_port() - tub.listenOn("tcp:%d" % portnum) - tub.setLocation("localhost:%d" % portnum) - + listenOnUnused(tub) c = IntroducerClient(tub, self.introducer_furl, u"nickname-client", "version", "oldest", {}, fakeseq, FilePath(self.mktemp()))