Ticket #4881: patch.2.txt

File patch.2.txt, 8.7 KB (added by lewq, 3 years ago)

With docstrings

Line 
1Index: twisted/internet/test/test_process.py
2===================================================================
3--- twisted/internet/test/test_process.py       (revision 30737)
4+++ twisted/internet/test/test_process.py       (working copy)
5@@ -20,9 +20,9 @@
6 from twisted.internet.defer import Deferred, succeed
7 from twisted.internet.protocol import ProcessProtocol
8 from twisted.internet.error import ProcessDone, ProcessTerminated
9+from twisted.python import runtime
10+from twisted.internet import process
11 
12-
13-
14 class _ShutdownCallbackProcessProtocol(ProcessProtocol):
15     """
16     An L{IProcessProtocol} which fires a Deferred when the process it is
17@@ -590,3 +590,135 @@
18             "Twisted 10.0.0: There is no longer any potential for zombie "
19             "process.")
20         self.assertEquals(len(warnings), 1)
21+
22+
23+class FakeResourceModule(object):
24+    RLIMIT_NOFILE = 1
25+    def getrlimit(self, no):
26+        return [0, 9223372036854775808]
27+        # TODO: Test that this gets rounded down
28+
29+class FDDetectorTest(TestCase):
30+    """
31+    Tests for _FDDetector class in twisted.internet.process, which detects
32+    which function to drop in place for the _listOpenFDs method.
33+    """
34+    skip = runtime.platform.getType() == 'win32'
35+    sane = False
36+    procfs = False
37+    devfs = False
38+    opened_file = False
39+    saved_resource_module = None
40+
41+    def getpid(self):
42+        return 123
43+
44+    def listdir(self, arg):
45+        if arg == '/proc/123/fd':
46+            if self.procfs:
47+                return ["0","1","2"]
48+            else:
49+                raise OSError
50+
51+        if arg == '/dev/fd':
52+            if self.devfs:
53+                if not self.sane:
54+                    # Always return the same thing
55+                    return ["0","1","2"]
56+                else:
57+                    if self.opened_file:
58+                        return ["0","1","2","3"]
59+                    else:
60+                        return ["0","1","2"]
61+            else:
62+                raise OSError
63+
64+    def openfile(self, fname, mode):
65+        self.opened_file = True
66+
67+    def save_resource_module(self):
68+        try:
69+            import resource
70+            self.saved_resource_module = resource
71+        except:
72+            self.saved_resource_module = None
73+
74+    def hide_resource_module(self):
75+        import sys
76+        sys.modules['resource'] = None
77+
78+    def reveal_resource_module(self):
79+        import sys
80+        sys.modules['resource'] = FakeResourceModule()
81+
82+    def replace_resource_module(self):
83+        sys.modules['resource'] = self.saved_resource_module
84+
85+    def setUp(self):
86+        self.detector = process._FDDetector()
87+        self.detector.listdir = self.listdir
88+        self.detector.getpid = self.getpid
89+        self.detector.openfile = self.openfile
90+
91+    def test_fddetector_sane_devfd(self):
92+        """
93+        e.g., FreeBSD with fdescfs mounted
94+        """
95+        self.procfs = False
96+        self.devfs = True
97+        self.sane = True
98+        self.assertIdentical(
99+                self.detector._getImplementation(),
100+                process._devFDImplementation
101+                )
102+
103+    def test_fddetector_insane_devfd(self):
104+        """
105+        e.g., FreeBSD without fdescfs mounted
106+        """
107+        self.procfs = False
108+        self.devfs = True
109+        self.sane = False
110+        self.assertIdentical(
111+                self.detector._getImplementation(),
112+                process._fallbackFDImplementation
113+                )
114+
115+    def test_fddetector_procfd(self):
116+        """
117+        e.g., Linux
118+        """
119+        self.devfs = False
120+        self.procfs = True
121+        self.assertIdentical(
122+                self.detector._getImplementation(),
123+                process._procFDImplementation
124+                )
125+
126+    def test_fddetector_resource_importable(self):
127+        """
128+        e.g., IRIX?
129+        """
130+        self.devfs = False
131+        self.procfs = False
132+        self.resource_importable = True
133+        self.reveal_resource_module()
134+        self.assertIdentical(
135+                self.detector._getImplementation(),
136+                process._resourceFDImplementation
137+                )
138+        self.replace_resource_module()
139+
140+    def test_fddetector_resource_unimportable(self):
141+        """
142+        e.g., who knows
143+        """
144+        self.devfs = False
145+        self.procfs = False
146+        self.hide_resource_module()
147+        self.assertIdentical(
148+                self.detector._getImplementation(),
149+                process._fallbackFDImplementation
150+                )
151+        self.replace_resource_module()
152+
153Index: twisted/internet/process.py
154===================================================================
155--- twisted/internet/process.py (revision 30737)
156+++ twisted/internet/process.py (working copy)
157@@ -466,35 +466,102 @@
158         return "<%s pid=%s status=%s>" % (self.__class__.__name__,
159                                           self.pid, self.status)
160 
161+def _listOpenFDs():
162+    """
163+    Runs _setFDImplementation, which detects in a system-dependent way
164+    which implementation to monkey-patch in to replace this function
165+    """
166+    return _setFDImplementation()
167 
168+def _setFDImplementation():
169+    """
170+    Instanciate a (testable) _FDDetector which actually does the work.
171+    """
172+    detector = _FDDetector()
173+    _listOpenFDs = detector._getImplementation()
174+    return _listOpenFDs()
175 
176-def _listOpenFDs():
177+def _devFDImplementation():
178     """
179-    Return an iterable of potentially open file descriptors.
180+    Simple implementation for systems where /dev/fd actually works.
181+    """
182+    dname = "/dev/fd"
183+    result = [int(fd) for fd in os.listdir(dname)]
184+    return result
185 
186-    This function returns an iterable over the contents of /dev/fd or
187-    /proc/<pid>/fd, if they're available on the platform. If they're not, the
188-    returned value is the range [0, maxfds], where 'maxfds' is at least 256.
189+def _procFDImplementation():
190     """
191-    dname = "/dev/fd"
192-    try:
193-        return [int(fd) for fd in os.listdir(dname)]
194-    except:
195-        dname = "/proc/%d/fd" % (os.getpid(),)
196+    Simple implementation for systems where /proc/pid/fd exists
197+    (we assume it works).
198+    """
199+    dname = "/proc/%d/fd" % (os.getpid(),)
200+    return [int(fd) for fd in os.listdir(dname)]
201+
202+def _resourceFDImplementation():
203+    """
204+    Fallback implementation where the resource module can inform us
205+    about how many FDs we can expect.
206+    """
207+    import resource
208+    maxfds = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + 1
209+    # OS-X reports 9223372036854775808. That's a lot of fds
210+    # to close
211+    if maxfds > 1024:
212+        maxfds = 1024
213+    return xrange(maxfds)
214+
215+def _fallbackFDImplementation():
216+    """
217+    Fallback-fallback implementation where we just assume that we need
218+    to close 256 FDs.
219+    """
220+    maxfds = 256
221+    return xrange(maxfds)
222+
223+class _FDDetector(object):
224+    """
225+    Contains the logic on how to detect which of the above implementations
226+    to use in a platform-specific way.
227+    """
228+    # So that we can unit test this
229+    listdir = os.listdir
230+    getpid = os.getpid
231+    openfile = open
232+
233+    def _getImplementation(self):
234+        """
235+        Check if /dev/fd works, if so, use that
236+        Otherwise, check if /proc/%d/fd exists, if so use that
237+        Otherwise, ask resource.getrlimit, if that throws an exception, then
238+        fallback to _fallbackFDImplementation
239+        """
240         try:
241-            return [int(fd) for fd in os.listdir(dname)]
242+            self.listdir("/dev/fd")
243+            if self._checkDevFDSanity(): # FreeBSD support :-)
244+                return _devFDImplementation
245+            else:
246+                return _fallbackFDImplementation
247+
248         except:
249             try:
250-                import resource
251-                maxfds = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + 1
252-                # OS-X reports 9223372036854775808. That's a lot of fds
253-                # to close
254-                if maxfds > 1024:
255-                    maxfds = 1024
256+                self.listdir("/proc/%d/fd" % (self.getpid(),))
257+                return _procFDImplementation
258             except:
259-                maxfds = 256
260-            return xrange(maxfds)
261+                try:
262+                    _resourceFDImplementation() # Imports resource
263+                    return _resourceFDImplementation
264+                except:
265+                    return _fallbackFDImplementation
266 
267+    def _checkDevFDSanity(self):
268+        """
269+        Returns true iff opening a file modifies the fds visible
270+        in /dev/fd, as it should on a sane platform.
271+        """
272+        start = self.listdir("/dev/fd")
273+        fp = self.openfile("/dev/null", "r")
274+        end = self.listdir("/dev/fd")
275+        return start != end
276 
277 
278 class Process(_BaseProcess):