source: trunk/src/allmydata/util/iputil.py

Last change on this file was 4da491a, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-03-11T20:37:27Z

remove more usage of "future"

  • Property mode set to 100644
File size: 7.9 KB
Line 
1"""
2Utilities for getting IP addresses.
3"""
4
5from typing import Callable
6
7import os, socket
8
9from zope.interface import implementer
10
11import attr
12
13from netifaces import (
14    interfaces,
15    ifaddresses,
16)
17
18# from Twisted
19from twisted.python.reflect import requireModule
20from twisted.python import log
21from twisted.internet.endpoints import AdoptedStreamServerEndpoint
22from twisted.internet.interfaces import (
23    IReactorSocket,
24    IStreamServerEndpoint,
25)
26
27from .gcutil import (
28    fileDescriptorResource,
29)
30
31fcntl = requireModule("fcntl")
32
33allocate_tcp_port: Callable[[], int]
34from foolscap.util import allocate_tcp_port # re-exported
35
36try:
37    import resource
38    def increase_rlimits():
39        # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain
40        # systems (OS-X, probably solaris) start with a relatively low limit
41        # (256), and some unit tests want to open up more sockets than this.
42        # Most linux systems start with both hard and soft limits at 1024,
43        # which is plenty.
44
45        # unfortunately the values to pass to setrlimit() vary widely from
46        # one system to another. OS-X reports (256, HUGE), but the real hard
47        # limit is 10240, and accepts (-1,-1) to mean raise it to the
48        # maximum. Cygwin reports (256, -1), then ignores a request of
49        # (-1,-1): instead you have to guess at the hard limit (it appears to
50        # be 3200), so using (3200,-1) seems to work. Linux reports a
51        # sensible (1024,1024), then rejects (-1,-1) as trying to raise the
52        # maximum limit, so you could set it to (1024,1024) but you might as
53        # well leave it alone.
54
55        try:
56            current = resource.getrlimit(resource.RLIMIT_NOFILE)
57        except AttributeError:
58            # we're probably missing RLIMIT_NOFILE
59            return
60
61        if current[0] >= 1024:
62            # good enough, leave it alone
63            return
64
65        try:
66            if current[1] > 0 and current[1] < 1000000:
67                # solaris reports (256, 65536)
68                resource.setrlimit(resource.RLIMIT_NOFILE,
69                                   (current[1], current[1]))
70            else:
71                # this one works on OS-X (bsd), and gives us 10240, but
72                # it doesn't work on linux (on which both the hard and
73                # soft limits are set to 1024 by default).
74                resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1))
75                new = resource.getrlimit(resource.RLIMIT_NOFILE)
76                if new[0] == current[0]:
77                    # probably cygwin, which ignores -1. Use a real value.
78                    resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1))
79
80        except ValueError:
81            log.msg("unable to set RLIMIT_NOFILE: current value %s"
82                     % (resource.getrlimit(resource.RLIMIT_NOFILE),))
83        except:
84            # who knows what. It isn't very important, so log it and continue
85            log.err()
86except ImportError:
87    def _increase_rlimits():
88        # TODO: implement this for Windows.  Although I suspect the
89        # solution might be "be running under the iocp reactor and
90        # make this function be a no-op".
91        pass
92    # pyflakes complains about two 'def FOO' statements in the same time,
93    # since one might be shadowing the other. This hack appeases pyflakes.
94    increase_rlimits = _increase_rlimits
95
96
97def get_local_addresses_sync():
98    """
99    Get locally assigned addresses as dotted-quad native strings.
100
101    :return [str]: A list of IPv4 addresses which are assigned to interfaces
102        on the local system.
103    """
104    return list(
105        str(address["addr"])
106        for iface_name
107        in interfaces()
108        for address
109        in ifaddresses(iface_name).get(socket.AF_INET, [])
110    )
111
112
113def _foolscapEndpointForPortNumber(portnum):
114    """
115    Create an endpoint that can be passed to ``Tub.listen``.
116
117    :param portnum: Either an integer port number indicating which TCP/IPv4
118        port number the endpoint should bind or ``None`` to automatically
119        allocate such a port number.
120
121    :return: A two-tuple of the integer port number allocated and a
122        Foolscap-compatible endpoint object.
123    """
124    if portnum is None:
125        # Bury this reactor import here to minimize the chances of it having
126        # the effect of installing the default reactor.
127        from twisted.internet import reactor
128        if fcntl is not None and IReactorSocket.providedBy(reactor):
129            # On POSIX we can take this very safe approach of binding the
130            # actual socket to an address.  Once the bind succeeds here, we're
131            # no longer subject to any future EADDRINUSE problems.
132            s = socket.socket()
133            try:
134                s.bind(('', 0))
135                portnum = s.getsockname()[1]
136                s.listen(1)
137                # File descriptors are a relatively scarce resource.  The
138                # cleanup process for the file descriptor we're about to dup
139                # is unfortunately complicated.  In particular, it involves
140                # the Python garbage collector.  See CleanupEndpoint for
141                # details of that.  Here, we need to make sure the garbage
142                # collector actually runs frequently enough to make a
143                # difference.  Normally, the garbage collector is triggered by
144                # allocations.  It doesn't know about *file descriptor*
145                # allocation though.  So ... we'll "teach" it about those,
146                # here.
147                fileDescriptorResource.allocate()
148                fd = os.dup(s.fileno())
149                flags = fcntl.fcntl(fd, fcntl.F_GETFD)
150                flags = flags | os.O_NONBLOCK | fcntl.FD_CLOEXEC
151                fcntl.fcntl(fd, fcntl.F_SETFD, flags)
152                endpoint = AdoptedStreamServerEndpoint(reactor, fd, socket.AF_INET)
153                return (portnum, CleanupEndpoint(endpoint, fd))
154            finally:
155                s.close()
156        else:
157            # Get a random port number and fall through.  This is necessary on
158            # Windows where Twisted doesn't offer IReactorSocket.  This
159            # approach is error prone for the reasons described on
160            # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2787
161            portnum = allocate_tcp_port()
162    return (portnum, "tcp:%d" % portnum)
163
164
165@implementer(IStreamServerEndpoint)
166@attr.s
167class CleanupEndpoint(object):
168    """
169    An ``IStreamServerEndpoint`` wrapper which closes a file descriptor if the
170    wrapped endpoint is never used.
171
172    :ivar IStreamServerEndpoint _wrapped: The wrapped endpoint.  The
173        ``listen`` implementation is delegated to this object.
174
175    :ivar int _fd: The file descriptor to close if ``listen`` is never called
176        by the time this object is garbage collected.
177
178    :ivar bool _listened: A flag recording whether or not ``listen`` has been
179        called.
180    """
181    _wrapped = attr.ib()
182    _fd = attr.ib()
183    _listened = attr.ib(default=False)
184
185    def listen(self, protocolFactory):
186        self._listened = True
187        return self._wrapped.listen(protocolFactory)
188
189    def __del__(self):
190        """
191        If ``listen`` was never called then close the file descriptor.
192        """
193        if not self._listened:
194            os.close(self._fd)
195            fileDescriptorResource.release()
196
197
198def listenOnUnused(tub, portnum=None):
199    """
200    Start listening on an unused TCP port number with the given tub.
201
202    :param portnum: Either an integer port number indicating which TCP/IPv4
203        port number the endpoint should bind or ``None`` to automatically
204        allocate such a port number.
205
206    :return: An integer indicating the TCP port number on which the tub is now
207        listening.
208    """
209    portnum, endpoint = _foolscapEndpointForPortNumber(portnum)
210    tub.listenOn(endpoint)
211    tub.setLocation("localhost:%d" % portnum)
212    return portnum
213
214
215__all__ = ["allocate_tcp_port",
216           "increase_rlimits",
217           "get_local_addresses_sync",
218           "listenOnUnused",
219           ]
Note: See TracBrowser for help on using the repository browser.