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

Last change on this file was f47b45a, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-28T00:25:06Z

remove static native_str()

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