Skip to content

Commit 6459025

Browse files
committed
Issue #19170: telnetlib: use selectors.
1 parent 6633c39 commit 6459025

2 files changed

Lines changed: 100 additions & 274 deletions

File tree

Lib/telnetlib.py

Lines changed: 78 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
Note that read_all() won't read until eof -- it just reads some data
1818
-- but it guarantees to read at least one byte unless EOF is hit.
1919
20-
It is possible to pass a Telnet object to select.select() in order to
21-
wait until more data is available. Note that in this case,
22-
read_eager() may return b'' even if there was data on the socket,
23-
because the protocol negotiation may have eaten the data. This is why
24-
EOFError is needed in some cases to distinguish between "no data" and
25-
"connection closed" (since the socket also appears ready for reading
26-
when it is closed).
20+
It is possible to pass a Telnet object to a selector in order to wait until
21+
more data is available. Note that in this case, read_eager() may return b''
22+
even if there was data on the socket, because the protocol negotiation may have
23+
eaten the data. This is why EOFError is needed in some cases to distinguish
24+
between "no data" and "connection closed" (since the socket also appears ready
25+
for reading when it is closed).
2726
2827
To do:
2928
- option negotiation
@@ -34,10 +33,9 @@
3433

3534

3635
# Imported modules
37-
import errno
3836
import sys
3937
import socket
40-
import select
38+
import selectors
4139

4240
__all__ = ["Telnet"]
4341

@@ -130,6 +128,15 @@
130128
EXOPL = bytes([255]) # Extended-Options-List
131129
NOOPT = bytes([0])
132130

131+
132+
# poll/select have the advantage of not requiring any extra file descriptor,
133+
# contrarily to epoll/kqueue (also, they require a single syscall).
134+
if hasattr(selectors, 'PollSelector'):
135+
_TelnetSelector = selectors.PollSelector
136+
else:
137+
_TelnetSelector = selectors.SelectSelector
138+
139+
133140
class Telnet:
134141

135142
"""Telnet interface class.
@@ -206,7 +213,6 @@ def __init__(self, host=None, port=0,
206213
self.sb = 0 # flag for SB and SE sequence.
207214
self.sbdataq = b''
208215
self.option_callback = None
209-
self._has_poll = hasattr(select, 'poll')
210216
if host is not None:
211217
self.open(host, port, timeout)
212218

@@ -288,61 +294,6 @@ def read_until(self, match, timeout=None):
288294
possibly the empty string. Raise EOFError if the connection
289295
is closed and no cooked data is available.
290296
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 OSError 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().
346297
"""
347298
n = len(match)
348299
self.process_rawq()
@@ -352,27 +303,26 @@ def _read_until_with_select(self, match, timeout=None):
352303
buf = self.cookedq[:i]
353304
self.cookedq = self.cookedq[i:]
354305
return buf
355-
s_reply = ([self], [], [])
356-
s_args = s_reply
357306
if timeout is not None:
358-
s_args = s_args + (timeout,)
359307
from time import time
360-
time_start = time()
361-
while not self.eof and select.select(*s_args) == s_reply:
362-
i = max(0, len(self.cookedq)-n)
363-
self.fill_rawq()
364-
self.process_rawq()
365-
i = self.cookedq.find(match, i)
366-
if i >= 0:
367-
i = i+n
368-
buf = self.cookedq[:i]
369-
self.cookedq = self.cookedq[i:]
370-
return buf
371-
if timeout is not None:
372-
elapsed = time() - time_start
373-
if elapsed >= timeout:
374-
break
375-
s_args = s_reply + (timeout-elapsed,)
308+
deadline = time() + timeout
309+
with _TelnetSelector() as selector:
310+
selector.register(self, selectors.EVENT_READ)
311+
while not self.eof:
312+
if selector.select(timeout):
313+
i = max(0, len(self.cookedq)-n)
314+
self.fill_rawq()
315+
self.process_rawq()
316+
i = self.cookedq.find(match, i)
317+
if i >= 0:
318+
i = i+n
319+
buf = self.cookedq[:i]
320+
self.cookedq = self.cookedq[i:]
321+
return buf
322+
if timeout is not None:
323+
timeout = deadline - time()
324+
if timeout < 0:
325+
break
376326
return self.read_very_lazy()
377327

378328
def read_all(self):
@@ -577,29 +527,35 @@ def fill_rawq(self):
577527

578528
def sock_avail(self):
579529
"""Test whether data is available on the socket."""
580-
return select.select([self], [], [], 0) == ([self], [], [])
530+
with _TelnetSelector() as selector:
531+
selector.register(self, selectors.EVENT_READ)
532+
return bool(selector.select(0))
581533

582534
def interact(self):
583535
"""Interaction function, emulates a very dumb telnet client."""
584536
if sys.platform == "win32":
585537
self.mt_interact()
586538
return
587-
while 1:
588-
rfd, wfd, xfd = select.select([self, sys.stdin], [], [])
589-
if self in rfd:
590-
try:
591-
text = self.read_eager()
592-
except EOFError:
593-
print('*** Connection closed by remote host ***')
594-
break
595-
if text:
596-
sys.stdout.write(text.decode('ascii'))
597-
sys.stdout.flush()
598-
if sys.stdin in rfd:
599-
line = sys.stdin.readline().encode('ascii')
600-
if not line:
601-
break
602-
self.write(line)
539+
with _TelnetSelector() as selector:
540+
selector.register(self, selectors.EVENT_READ)
541+
selector.register(sys.stdin, selectors.EVENT_READ)
542+
543+
while True:
544+
for key, events in selector.select():
545+
if key.fileobj is self:
546+
try:
547+
text = self.read_eager()
548+
except EOFError:
549+
print('*** Connection closed by remote host ***')
550+
return
551+
if text:
552+
sys.stdout.write(text.decode('ascii'))
553+
sys.stdout.flush()
554+
elif key.fileobj is sys.stdin:
555+
line = sys.stdin.readline().encode('ascii')
556+
if not line:
557+
return
558+
self.write(line)
603559

604560
def mt_interact(self):
605561
"""Multithreaded version of interact()."""
@@ -645,79 +601,6 @@ def expect(self, list, timeout=None):
645601
or if more than one expression can match the same input, the
646602
results are undeterministic, and may depend on the I/O timing.
647603
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 OSError 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().
721604
"""
722605
re = None
723606
list = list[:]
@@ -728,27 +611,27 @@ def _expect_with_select(self, list, timeout=None):
728611
list[i] = re.compile(list[i])
729612
if timeout is not None:
730613
from time import time
731-
time_start = time()
732-
while 1:
733-
self.process_rawq()
734-
for i in indices:
735-
m = list[i].search(self.cookedq)
736-
if m:
737-
e = m.end()
738-
text = self.cookedq[:e]
739-
self.cookedq = self.cookedq[e:]
740-
return (i, m, text)
741-
if self.eof:
742-
break
743-
if timeout is not None:
744-
elapsed = time() - time_start
745-
if elapsed >= timeout:
746-
break
747-
s_args = ([self.fileno()], [], [], timeout-elapsed)
748-
r, w, x = select.select(*s_args)
749-
if not r:
750-
break
751-
self.fill_rawq()
614+
deadline = time() + timeout
615+
with _TelnetSelector() as selector:
616+
selector.register(self, selectors.EVENT_READ)
617+
while not self.eof:
618+
self.process_rawq()
619+
for i in indices:
620+
m = list[i].search(self.cookedq)
621+
if m:
622+
e = m.end()
623+
text = self.cookedq[:e]
624+
self.cookedq = self.cookedq[e:]
625+
return (i, m, text)
626+
if timeout is not None:
627+
ready = selector.select(timeout)
628+
timeout = deadline - time()
629+
if not ready:
630+
if timeout < 0:
631+
break
632+
else:
633+
continue
634+
self.fill_rawq()
752635
text = self.read_very_lazy()
753636
if not text and self.eof:
754637
raise EOFError

0 commit comments

Comments
 (0)