Skip to content

Commit dad5711

Browse files
committed
Fixes Issue #14635: telnetlib will use poll() rather than select() when possible
to avoid failing due to the select() file descriptor limit.
1 parent 4774946 commit dad5711

4 files changed

Lines changed: 223 additions & 7 deletions

File tree

Lib/telnetlib.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535

3636
# Imported modules
37+
import errno
3738
import sys
3839
import socket
3940
import select
@@ -205,6 +206,7 @@ def __init__(self, host=None, port=0,
205206
self.sb = 0 # flag for SB and SE sequence.
206207
self.sbdataq = b''
207208
self.option_callback = None
209+
self._has_poll = hasattr(select, 'poll')
208210
if host is not None:
209211
self.open(host, port, timeout)
210212

@@ -286,6 +288,61 @@ def read_until(self, match, timeout=None):
286288
possibly the empty string. Raise EOFError if the connection
287289
is closed and no cooked data is available.
288290
291+
"""
292+
if self._has_poll:
293+
return self._read_until_with_poll(match, timeout)
294+
else:
295+
return self._read_until_with_select(match, timeout)
296+
297+
def _read_until_with_poll(self, match, timeout):
298+
"""Read until a given string is encountered or until timeout.
299+
300+
This method uses select.poll() to implement the timeout.
301+
"""
302+
n = len(match)
303+
call_timeout = timeout
304+
if timeout is not None:
305+
from time import time
306+
time_start = time()
307+
self.process_rawq()
308+
i = self.cookedq.find(match)
309+
if i < 0:
310+
poller = select.poll()
311+
poll_in_or_priority_flags = select.POLLIN | select.POLLPRI
312+
poller.register(self, poll_in_or_priority_flags)
313+
while i < 0 and not self.eof:
314+
try:
315+
ready = poller.poll(call_timeout)
316+
except select.error as e:
317+
if e.errno == errno.EINTR:
318+
if timeout is not None:
319+
elapsed = time() - time_start
320+
call_timeout = timeout-elapsed
321+
continue
322+
raise
323+
for fd, mode in ready:
324+
if mode & poll_in_or_priority_flags:
325+
i = max(0, len(self.cookedq)-n)
326+
self.fill_rawq()
327+
self.process_rawq()
328+
i = self.cookedq.find(match, i)
329+
if timeout is not None:
330+
elapsed = time() - time_start
331+
if elapsed >= timeout:
332+
break
333+
call_timeout = timeout-elapsed
334+
poller.unregister(self)
335+
if i >= 0:
336+
i = i + n
337+
buf = self.cookedq[:i]
338+
self.cookedq = self.cookedq[i:]
339+
return buf
340+
return self.read_very_lazy()
341+
342+
def _read_until_with_select(self, match, timeout=None):
343+
"""Read until a given string is encountered or until timeout.
344+
345+
The timeout is implemented using select.select().
289346
"""
290347
n = len(match)
291348
self.process_rawq()
@@ -588,6 +645,79 @@ def expect(self, list, timeout=None):
588645
or if more than one expression can match the same input, the
589646
results are undeterministic, and may depend on the I/O timing.
590647
648+
"""
649+
if self._has_poll:
650+
return self._expect_with_poll(list, timeout)
651+
else:
652+
return self._expect_with_select(list, timeout)
653+
654+
def _expect_with_poll(self, expect_list, timeout=None):
655+
"""Read until one from a list of a regular expressions matches.
656+
657+
This method uses select.poll() to implement the timeout.
658+
"""
659+
re = None
660+
expect_list = expect_list[:]
661+
indices = range(len(expect_list))
662+
for i in indices:
663+
if not hasattr(expect_list[i], "search"):
664+
if not re: import re
665+
expect_list[i] = re.compile(expect_list[i])
666+
call_timeout = timeout
667+
if timeout is not None:
668+
from time import time
669+
time_start = time()
670+
self.process_rawq()
671+
m = None
672+
for i in indices:
673+
m = expect_list[i].search(self.cookedq)
674+
if m:
675+
e = m.end()
676+
text = self.cookedq[:e]
677+
self.cookedq = self.cookedq[e:]
678+
break
679+
if not m:
680+
poller = select.poll()
681+
poll_in_or_priority_flags = select.POLLIN | select.POLLPRI
682+
poller.register(self, poll_in_or_priority_flags)
683+
while not m and not self.eof:
684+
try:
685+
ready = poller.poll(call_timeout)
686+
except select.error as e:
687+
if e.errno == errno.EINTR:
688+
if timeout is not None:
689+
elapsed = time() - time_start
690+
call_timeout = timeout-elapsed
691+
continue
692+
raise
693+
for fd, mode in ready:
694+
if mode & poll_in_or_priority_flags:
695+
self.fill_rawq()
696+
self.process_rawq()
697+
for i in indices:
698+
m = expect_list[i].search(self.cookedq)
699+
if m:
700+
e = m.end()
701+
text = self.cookedq[:e]
702+
self.cookedq = self.cookedq[e:]
703+
break
704+
if timeout is not None:
705+
elapsed = time() - time_start
706+
if elapsed >= timeout:
707+
break
708+
call_timeout = timeout-elapsed
709+
poller.unregister(self)
710+
if m:
711+
return (i, m, text)
712+
text = self.read_very_lazy()
713+
if not text and self.eof:
714+
raise EOFError
715+
return (-1, None, text)
716+
717+
def _expect_with_select(self, list, timeout=None):
718+
"""Read until one from a list of a regular expressions matches.
719+
720+
The timeout is implemented using select.select().
591721
"""
592722
re = None
593723
list = list[:]

Lib/test/test_telnetlib.py

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ def testTimeoutOpen(self):
7575

7676
class SocketStub(object):
7777
''' a socket proxy that re-defines sendall() '''
78-
def __init__(self, reads=[]):
79-
self.reads = reads
78+
def __init__(self, reads=()):
79+
self.reads = list(reads) # Intentionally make a copy.
8080
self.writes = []
8181
self.block = False
8282
def sendall(self, data):
@@ -102,7 +102,7 @@ def msg(self, msg, *args):
102102
self._messages += out.getvalue()
103103
return
104104

105-
def new_select(*s_args):
105+
def mock_select(*s_args):
106106
block = False
107107
for l in s_args:
108108
for fob in l:
@@ -113,6 +113,30 @@ def new_select(*s_args):
113113
else:
114114
return s_args
115115

116+
class MockPoller(object):
117+
test_case = None # Set during TestCase setUp.
118+
119+
def __init__(self):
120+
self._file_objs = []
121+
122+
def register(self, fd, eventmask):
123+
self.test_case.assertTrue(hasattr(fd, 'fileno'), fd)
124+
self.test_case.assertEqual(eventmask, select.POLLIN|select.POLLPRI)
125+
self._file_objs.append(fd)
126+
127+
def poll(self, timeout=None):
128+
block = False
129+
for fob in self._file_objs:
130+
if isinstance(fob, TelnetAlike):
131+
block = fob.sock.block
132+
if block:
133+
return []
134+
else:
135+
return zip(self._file_objs, [select.POLLIN]*len(self._file_objs))
136+
137+
def unregister(self, fd):
138+
self._file_objs.remove(fd)
139+
116140
@contextlib.contextmanager
117141
def test_socket(reads):
118142
def new_conn(*ignored):
@@ -125,23 +149,36 @@ def new_conn(*ignored):
125149
socket.create_connection = old_conn
126150
return
127151

128-
def test_telnet(reads=[], cls=TelnetAlike):
152+
def test_telnet(reads=(), cls=TelnetAlike, use_poll=None):
129153
''' return a telnetlib.Telnet object that uses a SocketStub with
130154
reads queued up to be read '''
131155
for x in reads:
132156
assert type(x) is bytes, x
133157
with test_socket(reads):
134158
telnet = cls('dummy', 0)
135159
telnet._messages = '' # debuglevel output
160+
if use_poll is not None:
161+
if use_poll and not telnet._has_poll:
162+
raise unittest.SkipTest('select.poll() required.')
163+
telnet._has_poll = use_poll
136164
return telnet
137165

138-
class ReadTests(TestCase):
166+
167+
class ExpectAndReadTestCase(TestCase):
139168
def setUp(self):
140169
self.old_select = select.select
141-
select.select = new_select
170+
self.old_poll = select.poll
171+
select.select = mock_select
172+
select.poll = MockPoller
173+
MockPoller.test_case = self
174+
142175
def tearDown(self):
176+
MockPoller.test_case = None
177+
select.poll = self.old_poll
143178
select.select = self.old_select
144179

180+
181+
class ReadTests(ExpectAndReadTestCase):
145182
def test_read_until(self):
146183
"""
147184
read_until(expected, timeout=None)
@@ -158,6 +195,21 @@ def test_read_until(self):
158195
data = telnet.read_until(b'match')
159196
self.assertEqual(data, expect)
160197

198+
def test_read_until_with_poll(self):
199+
"""Use select.poll() to implement telnet.read_until()."""
200+
want = [b'x' * 10, b'match', b'y' * 10]
201+
telnet = test_telnet(want, use_poll=True)
202+
select.select = lambda *_: self.fail('unexpected select() call.')
203+
data = telnet.read_until(b'match')
204+
self.assertEqual(data, b''.join(want[:-1]))
205+
206+
def test_read_until_with_select(self):
207+
"""Use select.select() to implement telnet.read_until()."""
208+
want = [b'x' * 10, b'match', b'y' * 10]
209+
telnet = test_telnet(want, use_poll=False)
210+
select.poll = lambda *_: self.fail('unexpected poll() call.')
211+
data = telnet.read_until(b'match')
212+
self.assertEqual(data, b''.join(want[:-1]))
161213

162214
def test_read_all(self):
163215
"""
@@ -349,8 +401,38 @@ def test_debug_accepts_str_port(self):
349401
self.assertRegex(telnet._messages, r'0.*test')
350402

351403

404+
class ExpectTests(ExpectAndReadTestCase):
405+
def test_expect(self):
406+
"""
407+
expect(expected, [timeout])
408+
Read until the expected string has been seen, or a timeout is
409+
hit (default is no timeout); may block.
410+
"""
411+
want = [b'x' * 10, b'match', b'y' * 10]
412+
telnet = test_telnet(want)
413+
(_,_,data) = telnet.expect([b'match'])
414+
self.assertEqual(data, b''.join(want[:-1]))
415+
416+
def test_expect_with_poll(self):
417+
"""Use select.poll() to implement telnet.expect()."""
418+
want = [b'x' * 10, b'match', b'y' * 10]
419+
telnet = test_telnet(want, use_poll=True)
420+
select.select = lambda *_: self.fail('unexpected select() call.')
421+
(_,_,data) = telnet.expect([b'match'])
422+
self.assertEqual(data, b''.join(want[:-1]))
423+
424+
def test_expect_with_select(self):
425+
"""Use select.select() to implement telnet.expect()."""
426+
want = [b'x' * 10, b'match', b'y' * 10]
427+
telnet = test_telnet(want, use_poll=False)
428+
select.poll = lambda *_: self.fail('unexpected poll() call.')
429+
(_,_,data) = telnet.expect([b'match'])
430+
self.assertEqual(data, b''.join(want[:-1]))
431+
432+
352433
def test_main(verbose=None):
353-
support.run_unittest(GeneralTests, ReadTests, WriteTests, OptionTests)
434+
support.run_unittest(GeneralTests, ReadTests, WriteTests, OptionTests,
435+
ExpectTests)
354436

355437
if __name__ == '__main__':
356438
test_main()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ Chris Hoffman
410410
Albert Hofkamp
411411
Tomas Hoger
412412
Jonathan Hogg
413+
Akintayo Holder
413414
Gerrit Holl
414415
Shane Holloway
415416
Rune Holm

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ Core and Builtins
8787
Library
8888
-------
8989

90+
- Issue #14635: telnetlib will use poll() rather than select() when possible
91+
to avoid failing due to the select() file descriptor limit.
92+
9093
- Issue #15180: Clarify posixpath.join() error message when mixing str & bytes
9194

9295
- Issue #15230: runpy.run_path now correctly sets __package__ as described

0 commit comments

Comments
 (0)