Skip to content

Commit 4565981

Browse files
authored
telnetlib3-client --connect-timeout (jquast#113)
* --connect-timeout, closes #30 * threw in typing fixes, too!
1 parent c3bcaa4 commit 4565981

20 files changed

Lines changed: 151 additions & 56 deletions

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
Introduction
3030
============
3131

32-
``telnetlib3`` is a feature-rich Telnet Server and Client and Protocol library for Python 3.9 and newer.
32+
``telnetlib3`` is a feature-rich Telnet Server and Client Protocol library
33+
for Python 3.9 and newer.
3334

3435
This library supports both modern asyncio_ *and* legacy `Blocking API`_.
3536

docs/history.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
History
22
=======
3+
*unreleased*
4+
* new: ``connect_timeout`` arguments for client and ``--connect-timeout`` Client CLI argument.
5+
36
2.2.0
47
* bugfix: workaround for Microsoft Telnet client crash on
58
``SB NEW_ENVIRON SEND``, :ghissue:`24`. Server now defers ``DO

pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,7 @@ ignore_missing_imports = true
139139

140140
[[tool.mypy.overrides]]
141141
module = ["telnetlib3.tests.*"]
142-
disallow_untyped_defs = false
143-
disallow_incomplete_defs = false
144-
disallow_untyped_calls = false
145-
warn_return_any = false
142+
ignore_errors = true
146143

147144
[[tool.mypy.overrides]]
148145
module = ["telnetlib3.telnetlib"]

telnetlib3/client.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ async def open_connection( # pylint: disable=too-many-locals
390390
shell: Optional[ShellCallback] = None,
391391
connect_minwait: float = 2.0,
392392
connect_maxwait: float = 3.0,
393+
connect_timeout: Optional[float] = None,
393394
waiter_closed: Optional[asyncio.Future[None]] = None,
394395
_waiter_connected: Optional[asyncio.Future[None]] = None,
395396
limit: Optional[int] = None,
@@ -445,6 +446,11 @@ async def open_connection( # pylint: disable=too-many-locals
445446
otherwise confused by our demands, the shell continues anyway after the
446447
greater of this value has elapsed. A client that is not answering
447448
option negotiation will delay the start of the shell by this amount.
449+
:param connect_timeout: Timeout in seconds for the TCP connection to be
450+
established. When ``None`` (default), no timeout is applied and the
451+
connection attempt may block indefinitely. When specified, a
452+
:exc:`ConnectionError` is raised if the connection is not established
453+
within the given time.
448454
449455
:param force_binary: When ``True``, the encoding is used regardless
450456
of BINARY mode negotiation.
@@ -480,14 +486,22 @@ def connection_factory() -> client_base.BaseClient:
480486
send_environ=send_environ,
481487
)
482488

483-
_, protocol = await asyncio.get_event_loop().create_connection(
484-
connection_factory,
485-
host or "localhost",
486-
port,
487-
family=family,
488-
flags=flags,
489-
local_addr=local_addr,
490-
)
489+
try:
490+
_, protocol = await asyncio.wait_for(
491+
asyncio.get_event_loop().create_connection(
492+
connection_factory,
493+
host or "localhost",
494+
port,
495+
family=family,
496+
flags=flags,
497+
local_addr=local_addr,
498+
),
499+
timeout=connect_timeout,
500+
)
501+
except asyncio.TimeoutError as exc:
502+
raise ConnectionError(
503+
f"TCP connection to {host or 'localhost'}:{port}" f" timed out after {connect_timeout}s"
504+
) from exc
491505

492506
await protocol._waiter_connected # pylint: disable=protected-access
493507

@@ -518,6 +532,7 @@ async def run_client() -> None:
518532
"force_binary": args["force_binary"],
519533
"encoding_errors": args["encoding_errors"],
520534
"connect_minwait": args["connect_minwait"],
535+
"connect_timeout": args["connect_timeout"],
521536
"send_environ": args["send_environ"],
522537
}
523538

@@ -564,6 +579,12 @@ def _get_argument_parser() -> argparse.ArgumentParser:
564579
type=float,
565580
help="timeout for pending negotiation",
566581
)
582+
parser.add_argument(
583+
"--connect-timeout",
584+
default=None,
585+
type=float,
586+
help="timeout for TCP connection (seconds, default: no timeout)",
587+
)
567588
parser.add_argument(
568589
"--send-environ",
569590
default="TERM,LANG,COLUMNS,LINES,COLORTERM",
@@ -586,6 +607,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
586607
"force_binary": args.force_binary,
587608
"encoding_errors": args.encoding_errors,
588609
"connect_minwait": args.connect_minwait,
610+
"connect_timeout": args.connect_timeout,
589611
"send_environ": tuple(v.strip() for v in args.send_environ.split(",") if v.strip()),
590612
}
591613

telnetlib3/client_shell.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ class Terminal:
4343
"ModeDef", ["iflag", "oflag", "cflag", "lflag", "ispeed", "ospeed", "cc"]
4444
)
4545

46-
def __init__(
47-
self, telnet_writer: Union[TelnetWriter, TelnetWriterUnicode]
48-
) -> None:
46+
def __init__(self, telnet_writer: Union[TelnetWriter, TelnetWriterUnicode]) -> None:
4947
self.telnet_writer = telnet_writer
5048
self._fileno = sys.stdin.fileno()
5149
self._istty = os.path.sameopenfile(0, 1)

telnetlib3/fingerprinting_display.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ def _normalize_color_hex(hex_color: str) -> str:
919919
)
920920

921921
r, g, b = hex_to_rgb(hex_color)
922-
return rgb_to_hex(r, g, b)
922+
return str(rgb_to_hex(r, g, b))
923923

924924

925925
def _filter_terminal_detail( # pylint: disable=too-complex,too-many-branches

telnetlib3/server_pty_shell.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,7 @@ async def _bridge_loop(
379379
telnet_task: asyncio.Task[Union[bytes, str]] = asyncio.create_task(
380380
self.reader.read(4096)
381381
)
382-
pty_task: asyncio.Task[bool] = asyncio.create_task(
383-
pty_read_event.wait()
384-
)
382+
pty_task: asyncio.Task[bool] = asyncio.create_task(pty_read_event.wait())
385383

386384
done, pending = await asyncio.wait(
387385
{telnet_task, pty_task},

telnetlib3/stream_writer.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,9 +1456,7 @@ def handle_environ(self, env: dict[str, str]) -> None:
14561456
"""Receive environment variables as dict, :rfc:`1572`."""
14571457
self.log.debug("Environment values are %r", env)
14581458

1459-
def handle_send_client_environ(
1460-
self, _keys: Any
1461-
) -> dict[str, str]:
1459+
def handle_send_client_environ(self, _keys: Any) -> dict[str, str]:
14621460
"""
14631461
Send environment variables as dict, :rfc:`1572`.
14641462

telnetlib3/sync.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class TelnetConnection:
5959
:param port: Remote server port (default 23).
6060
:param timeout: Default timeout for operations in seconds.
6161
:param encoding: Character encoding (default 'utf8').
62+
:param connect_timeout: Timeout in seconds for the TCP connection to be
63+
established. Passed to ``telnetlib3.open_connection()``.
6264
:param kwargs: Additional arguments passed to ``telnetlib3.open_connection()``.
6365
6466
Example::

telnetlib3/tests/test_core.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
import telnetlib3
1717
from telnetlib3.telopt import (
1818
DO,
19-
IS,
2019
SB,
21-
SE,
2220
IAC,
2321
SGA,
2422
ECHO,
@@ -28,7 +26,6 @@
2826
TTYPE,
2927
BINARY,
3028
CHARSET,
31-
NEW_ENVIRON,
3229
)
3330
from telnetlib3.tests.accessories import ( # pylint: disable=unused-import
3431
bind_host,

0 commit comments

Comments
 (0)