Skip to content

Commit 18be1a1

Browse files
committed
Remove telnet_input module
1 parent dedb928 commit 18be1a1

4 files changed

Lines changed: 212 additions & 605 deletions

File tree

gambaterm/telnet.py

Lines changed: 32 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from __future__ import annotations
22

3-
import os
4-
import re
53
import time
64
import hashlib
75
import asyncio
86
import argparse
97
import traceback
10-
from contextlib import AbstractContextManager as ContextManager, nullcontext
8+
from contextlib import AbstractContextManager as ContextManager
119
from pathlib import Path
1210
from typing import TYPE_CHECKING, Any, Callable, Coroutine
1311
from concurrent.futures import ThreadPoolExecutor
@@ -24,33 +22,18 @@
2422
from .main import add_base_arguments, add_optional_arguments, AppConfig
2523
from .console import Console, GameboyColor
2624
from .input_getter import BaseInputGetter
27-
from .keyboard_input import MESSAGE_SUGGESTING_KITTY_SUPPORT
28-
from .telnet_input import TelnetInputState, read_telnet_input
25+
from .keyboard_input import (
26+
MESSAGE_SUGGESTING_KITTY_SUPPORT,
27+
console_input_from_keyboard_protocol_context,
28+
is_kitty_keyboard_protocol_supported,
29+
)
2930
from .remote_terminal import RemoteTerminal
3031
from .telnet_app_session import (
3132
set_tcp_nodelay,
3233
telnet_to_terminal,
3334
)
3435

3536

36-
class TelnetInputGetter(BaseInputGetter):
37-
"""Input getter that reads from telnet input state."""
38-
39-
def __init__(
40-
self, console: Console, terminal: RemoteTerminal, state: TelnetInputState
41-
) -> None:
42-
super().__init__(console, terminal)
43-
self._state = state
44-
45-
def get_pressed(self) -> set[Console.Input]:
46-
for event in self._state.pop_events():
47-
self._console.handle_event(event)
48-
return self._state.get_input()
49-
50-
def pop_keystrokes(self) -> list[Keystroke]:
51-
return []
52-
53-
5437
class NoInputGetter(BaseInputGetter):
5538
"""Input getter that returns no button presses."""
5639

@@ -77,7 +60,7 @@ def thread_target(
7760
console_callback: Callable[[], Console],
7861
app_config: AppConfig,
7962
color_mode: ColorMode,
80-
input_state: TelnetInputState | None = None,
63+
username: str | None,
8164
) -> int:
8265
"""Run the emulator in a thread with the given RemoteTerminal."""
8366
console: Console = console_callback()
@@ -88,18 +71,22 @@ def thread_target(
8871
] = console_input_from_file_context(
8972
console, term, app_config.input_file, app_config.skip_inputs
9073
)
91-
elif input_state is not None:
92-
console_input_context = nullcontext(
93-
TelnetInputGetter(console, term, input_state)
74+
elif is_kitty_keyboard_protocol_supported(term, timeout=3):
75+
console_input_context = console_input_from_keyboard_protocol_context(
76+
console,
77+
term,
9478
)
9579
else:
96-
console_input_context = nullcontext(NoInputGetter(console, term))
97-
98-
with console_input_context as get_console_input:
99-
try:
100-
term.stream.write(term.enter_fullscreen + term.clear + term.hide_cursor)
101-
term.stream.flush()
80+
message = MESSAGE_SUGGESTING_KITTY_SUPPORT
81+
term.stream.write(message)
82+
term.stream.flush()
83+
print(f"< User `{username}` did not support keyboard protocol")
84+
return 1
10285

86+
try:
87+
term.stream.write(term.enter_fullscreen + term.clear + term.hide_cursor)
88+
term.stream.flush()
89+
with console_input_context as get_console_input:
10390
run(
10491
console,
10592
input_getter=get_console_input,
@@ -109,58 +96,17 @@ def thread_target(
10996
break_after=app_config.break_after,
11097
speed_factor=app_config.speed_factor,
11198
)
112-
except (KeyboardInterrupt, OSError):
113-
return 0
114-
else:
115-
return 0
116-
finally:
117-
time.sleep(0.1)
118-
term.stream.write(term.clear + term.exit_fullscreen + term.normal_cursor)
119-
try:
120-
term.stream.flush()
121-
except BrokenPipeError:
122-
pass
123-
124-
125-
_KITTY_RESPONSE_RE = re.compile(rb"\x1b\[\?([0-9]*)u")
126-
127-
128-
async def _detect_kitty_keyboard(
129-
reader: TelnetReader, writer: TelnetWriter, timeout: float = 3.0
130-
) -> bool:
131-
"""Check if the telnet client supports the kitty keyboard protocol.
132-
133-
Must be called before the input reading task starts.
134-
135-
:param reader: telnetlib3 reader
136-
:param writer: telnetlib3 writer
137-
:param timeout: seconds to wait for response
138-
:returns: ``True`` if the terminal responds to the kitty keyboard query
139-
"""
140-
writer.write(b"\x1b[?u")
141-
await writer.drain()
142-
143-
buf = b""
144-
loop = asyncio.get_event_loop()
145-
deadline = loop.time() + timeout
146-
while True:
147-
remaining = deadline - loop.time()
148-
if remaining <= 0:
149-
break
99+
except (KeyboardInterrupt, OSError):
100+
return 0
101+
else:
102+
return 0
103+
finally:
104+
time.sleep(0.1)
105+
term.stream.write(term.clear + term.exit_fullscreen + term.normal_cursor)
150106
try:
151-
chunk = await asyncio.wait_for(
152-
reader.read(256),
153-
timeout=remaining,
154-
)
155-
if not chunk:
156-
break
157-
buf += chunk if isinstance(chunk, bytes) else chunk.encode("latin-1")
158-
if _KITTY_RESPONSE_RE.search(buf):
159-
return True
160-
except asyncio.TimeoutError:
161-
break
162-
163-
return False
107+
term.stream.flush()
108+
except BrokenPipeError:
109+
pass
164110

165111

166112
ShellCallback = Callable[["TelnetReader", "TelnetWriter"], Coroutine[Any, Any, None]]
@@ -313,25 +259,6 @@ async def _telnet_shell(
313259
# Set TCP_NODELAY to disable Nagle's algorithm for paced output
314260
set_tcp_nodelay(writer)
315261

316-
# Require kitty keyboard protocol (must be checked before input task starts)
317-
if getattr(app_config, "input_file", None) is None:
318-
if not await _detect_kitty_keyboard(reader, writer):
319-
print(
320-
f"< Telnet client {peer_host} does not support "
321-
f"kitty keyboard protocol"
322-
)
323-
msg = MESSAGE_SUGGESTING_KITTY_SUPPORT.replace("\n", "\r\n")
324-
writer.write(msg.encode("utf-8"))
325-
await writer.drain()
326-
return 1
327-
328-
# Create input pipe for Ctrl+C/D forwarding
329-
input_read_fd, input_write_fd = os.pipe()
330-
331-
state = TelnetInputState()
332-
input_task = asyncio.create_task(
333-
read_telnet_input(reader, writer, state, input_write_fd)
334-
)
335262
stats_task = asyncio.create_task(
336263
_log_connection_stats(writer, peer_host, peer_port)
337264
)
@@ -361,33 +288,20 @@ async def _telnet_shell(
361288
config = AppConfig(**vars(namespace))
362289

363290
def target(term: RemoteTerminal) -> int:
364-
return thread_target(term, console_callback, config, color_mode, state)
291+
return thread_target(term, console_callback, config, color_mode, username)
365292

366293
return await telnet_to_terminal(
294+
reader,
367295
writer,
368296
executor,
369297
target,
370-
input_read_fd,
371298
)
372299
finally:
373-
input_task.cancel()
374300
stats_task.cancel()
375-
try:
376-
await input_task
377-
except asyncio.CancelledError:
378-
pass
379301
try:
380302
await stats_task
381303
except asyncio.CancelledError:
382304
pass
383-
try:
384-
os.close(input_write_fd)
385-
except OSError:
386-
pass
387-
try:
388-
os.close(input_read_fd)
389-
except OSError:
390-
pass
391305

392306

393307
async def run_server(

gambaterm/telnet_app_session.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
if TYPE_CHECKING:
1414
from telnetlib3.stream_writer import TelnetWriter
15+
from telnetlib3.stream_reader import TelnetReader
1516

1617
from .remote_terminal import RemoteTerminal
1718

@@ -106,11 +107,30 @@ def on_naws(rows: int, cols: int) -> None:
106107
writer.set_ext_callback(NAWS, original_on_naws)
107108

108109

110+
async def async_reader_to_sync_pipe(reader: TelnetReader, write_fd: int) -> None:
111+
loop = asyncio.get_running_loop()
112+
pipe_file = os.fdopen(write_fd, "wb", buffering=0)
113+
transport, protocol = await loop.connect_write_pipe(
114+
lambda: asyncio.streams.FlowControlMixin(), pipe_file
115+
)
116+
writer = asyncio.StreamWriter(transport, protocol, None, loop)
117+
118+
try:
119+
while True:
120+
data = await reader.read(4096)
121+
if not data:
122+
break
123+
writer.write(data)
124+
await writer.drain()
125+
finally:
126+
writer.close()
127+
128+
109129
async def telnet_to_terminal(
130+
reader: TelnetReader,
110131
writer: TelnetWriter,
111132
executor: ThreadPoolExecutor,
112133
target: Callable[[RemoteTerminal], T],
113-
input_read_fd: int,
114134
) -> T:
115135
"""Create a RemoteTerminal and run *target* in a thread executor.
116136
@@ -125,29 +145,40 @@ async def telnet_to_terminal(
125145
cols = writer.get_extra_info("cols") or 80
126146
rows = writer.get_extra_info("rows") or 24
127147

128-
read_fd, write_fd = os.pipe()
148+
forward_read_fd, forward_write_fd = os.pipe()
129149
forward_task: asyncio.Task[None] | None = None
130150

151+
input_read_fd, input_write_fd = os.pipe()
152+
input_task: asyncio.Task[None] | None = None
153+
131154
def _target() -> T:
132-
with open(write_fd, "w", newline="\r\n") as stream:
133-
telnet_term = RemoteTerminal(
134-
stream=stream,
135-
keyboard_fd=input_read_fd,
136-
rows=rows,
137-
columns=cols,
138-
)
139-
with bind_resize_telnet(writer, telnet_term):
140-
return target(telnet_term)
155+
try:
156+
with open(forward_write_fd, "w", newline="\r\n") as stream:
157+
telnet_term = RemoteTerminal(
158+
stream=stream,
159+
keyboard_fd=input_read_fd,
160+
rows=rows,
161+
columns=cols,
162+
)
163+
with bind_resize_telnet(writer, telnet_term):
164+
return target(telnet_term)
165+
finally:
166+
os.close(input_read_fd)
141167

142168
loop = asyncio.get_running_loop()
143-
forward_task = asyncio.create_task(paced_forward_output(read_fd, writer))
169+
forward_task = asyncio.create_task(paced_forward_output(forward_read_fd, writer))
170+
input_task = asyncio.create_task(async_reader_to_sync_pipe(reader, input_write_fd))
144171
try:
145172
return await loop.run_in_executor(executor, _target)
146173
finally:
147174
# write_fd is closed by open(write_fd, "w").__exit__ in _target,
148175
# so forward_task will see EOF. Just wait for it to finish.
149-
if forward_task is not None:
150-
try:
151-
await asyncio.wait_for(forward_task, timeout=2.0)
152-
except asyncio.TimeoutError:
153-
forward_task.cancel()
176+
try:
177+
await asyncio.wait_for(forward_task, timeout=2.0)
178+
except asyncio.TimeoutError:
179+
forward_task.cancel()
180+
try:
181+
input_task.cancel()
182+
await asyncio.wait_for(input_task, timeout=2.0)
183+
except (asyncio.TimeoutError, asyncio.CancelledError):
184+
pass

0 commit comments

Comments
 (0)