From 544f6cec26bcda09905df026628b09953910b2b4 Mon Sep 17 00:00:00 2001 From: Ian Blenke Date: Wed, 12 Nov 2025 00:11:39 +0000 Subject: [PATCH 1/3] Adding no_tty --- rnsh/args.py | 4 +- rnsh/initiator.py | 138 +++++++++++++++++++++++++--------------------- rnsh/rnsh.py | 3 +- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/rnsh/args.py b/rnsh/args.py index 111f74c..2e2d0c9 100644 --- a/rnsh/args.py +++ b/rnsh/args.py @@ -23,7 +23,7 @@ def _split_array_at(arr: [_T], at: _T) -> ([_T], [_T]): [-b ] [-n] [-a ] ([-a ] ...) [-A | -C] [[--] [ ...]] rnsh [-c ] [-i ] [-v... | -q...] -p - rnsh [-c ] [-i ] [-v... | -q...] [-N] [-m] [-w ] + rnsh [-c ] [-i ] [-v... | -q...] [-N] [-m] [-w ] [-T] [[--] [ ...]] rnsh -h rnsh --version @@ -49,6 +49,7 @@ def _split_array_at(arr: [_T], at: _T) -> ([_T], [_T]): -C --no-remote-command Disable executing command line from remote -m --mirror Client returns with code of remote process -w TIME --timeout TIME Specify client connect and request timeout in seconds + -T --no-tty Force pipe mode (no TTY); useful for ProxyCommand -q --quiet Increase quietness (move level up), multiple increases effect DEFAULT LOGGING LEVEL CRITICAL (silent) @@ -106,6 +107,7 @@ def __init__(self, argv: [str]): self.program_args = args.get("", None) or [] self.no_id = args.get("--no-id", None) or False self.mirror = args.get("--mirror", None) or False + self.no_tty = args.get("--no-tty", None) or False timeout = args.get("--timeout", None) self.timeout = None try: diff --git a/rnsh/initiator.py b/rnsh/initiator.py index 8216b8c..3c3c366 100644 --- a/rnsh/initiator.py +++ b/rnsh/initiator.py @@ -226,13 +226,17 @@ async def _handle_error(errmsg: RNS.MessageBase): async def initiate(configdir: str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str, - timeout: float, command: [str] | None = None): + timeout: float, command: [str] | None = None, no_tty: bool = False): global _finished, _link log = _get_logger("_initiate") with process.TTYRestorer(sys.stdin.fileno()) as ttyRestorer: loop = asyncio.get_running_loop() state = InitiatorState.IS_INITIAL - data_buffer = bytearray(sys.stdin.buffer.read()) if not os.isatty(sys.stdin.fileno()) else bytearray() + # Determine pipe/TTY mode: force pipe if no_tty, otherwise auto-detect + is_stdin_pipe = no_tty or not os.isatty(sys.stdin.fileno()) + is_stdout_pipe = no_tty or not os.isatty(sys.stdout.fileno()) + is_stderr_pipe = no_tty or not os.isatty(sys.stderr.fileno()) + data_buffer = bytearray(sys.stdin.buffer.read()) if is_stdin_pipe else bytearray() line_buffer = bytearray() await _initiate_link( @@ -313,80 +317,86 @@ def stdin(): try: in_data = process.tty_read(sys.stdin.fileno()) if in_data is not None: - data = bytearray() - for b in bytes(in_data): - c = chr(b) - if c == "\r": - pre_esc = True - line_flush = True - data.append(b) - elif line_mode and c in flush_chars: - pre_esc = False - line_flush = True - data.append(b) - elif line_mode and (c == "\b" or c == "\x7f"): - pre_esc = False - if len(line_buffer)>0: - line_buffer.pop(-1) - blind_write_count -= 1 - os.write(1, "\b \b".encode("utf-8")) - elif pre_esc == True and c == "~": - pre_esc = False - esc = True - elif esc == True: - ret = handle_escape(c) - if ret != None: - if ret != "~": - data.append(ord("~")) - data.append(ord(ret)) - esc = False - else: - pre_esc = False - data.append(b) - - if not line_mode: - data_buffer.extend(data) + # In no-tty mode, skip escape sequence processing + if no_tty: + data_buffer.extend(in_data) else: - line_buffer.extend(data) - if line_flush: - data_buffer.extend(line_buffer) - line_buffer.clear() - os.write(1, ("\b \b"*blind_write_count).encode("utf-8")) - line_flush = False - blind_write_count = 0 + data = bytearray() + for b in bytes(in_data): + c = chr(b) + if c == "\r": + pre_esc = True + line_flush = True + data.append(b) + elif line_mode and c in flush_chars: + pre_esc = False + line_flush = True + data.append(b) + elif line_mode and (c == "\b" or c == "\x7f"): + pre_esc = False + if len(line_buffer)>0: + line_buffer.pop(-1) + blind_write_count -= 1 + os.write(1, "\b \b".encode("utf-8")) + elif pre_esc == True and c == "~": + pre_esc = False + esc = True + elif esc == True: + ret = handle_escape(c) + if ret != None: + if ret != "~": + data.append(ord("~")) + data.append(ord(ret)) + esc = False + else: + pre_esc = False + data.append(b) + + if not line_mode: + data_buffer.extend(data) else: - os.write(1, data) - blind_write_count += len(data) + line_buffer.extend(data) + if line_flush: + data_buffer.extend(line_buffer) + line_buffer.clear() + os.write(1, ("\b \b"*blind_write_count).encode("utf-8")) + line_flush = False + blind_write_count = 0 + else: + os.write(1, data) + blind_write_count += len(data) except EOFError: - if os.isatty(0): + if not is_stdin_pipe: data_buffer.extend(process.CTRL_D) stdin_eof = True process.tty_unset_reader_callbacks(sys.stdin.fileno()) process.tty_add_reader_callback(sys.stdin.fileno(), stdin) + # Skip terminal attribute gathering in no-tty mode tcattr = None rows, cols, hpix, vpix = (None, None, None, None) - try: - tcattr = termios.tcgetattr(0) - rows, cols, hpix, vpix = process.tty_get_winsize(0) - except: + if not no_tty: try: - tcattr = termios.tcgetattr(1) - rows, cols, hpix, vpix = process.tty_get_winsize(1) + tcattr = termios.tcgetattr(0) + rows, cols, hpix, vpix = process.tty_get_winsize(0) except: try: - tcattr = termios.tcgetattr(2) - rows, cols, hpix, vpix = process.tty_get_winsize(2) + tcattr = termios.tcgetattr(1) + rows, cols, hpix, vpix = process.tty_get_winsize(1) except: - pass + try: + tcattr = termios.tcgetattr(2) + rows, cols, hpix, vpix = process.tty_get_winsize(2) + except: + pass await _spin(lambda: channel.is_ready_to_send(), "Waiting for channel...", 1, quietness > 0) channel.send(protocol.ExecuteCommandMesssage(cmdline=command, - pipe_stdin=not os.isatty(0), - pipe_stdout=not os.isatty(1), - pipe_stderr=not os.isatty(2), + pipe_stdin=is_stdin_pipe, + pipe_stdout=is_stdout_pipe, + pipe_stderr=is_stderr_pipe, tcflags=tcattr, term=os.environ.get("TERM", None), rows=rows, @@ -394,7 +404,9 @@ def stdin(): hpix=hpix, vpix=vpix)) - loop.add_signal_handler(signal.SIGWINCH, sigwinch_handler) + # Skip window resize handling in no-tty mode + if not no_tty: + loop.add_signal_handler(signal.SIGWINCH, sigwinch_handler) _finished = asyncio.Event() loop.add_signal_handler(signal.SIGINT, functools.partial(_sigint_handler, signal.SIGINT, loop)) loop.add_signal_handler(signal.SIGTERM, functools.partial(_sigint_handler, signal.SIGTERM, loop)) @@ -412,7 +424,8 @@ def stdin(): if isinstance(message, protocol.StreamDataMessage): if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT: if message.data and len(message.data) > 0: - ttyRestorer.raw() + if not no_tty: + ttyRestorer.raw() log.debug(f"stdout: {message.data}") os.write(1, message.data) sys.stdout.flush() @@ -420,7 +433,8 @@ def stdin(): os.close(1) if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDERR: if message.data and len(message.data) > 0: - ttyRestorer.raw() + if not no_tty: + ttyRestorer.raw() log.debug(f"stdout: {message.data}") os.write(2, message.data) sys.stderr.flush() @@ -480,8 +494,8 @@ def compress_adaptive(buf: bytes): sent_eof = eof processed = True - # send window change, but rate limited - if winch and time.time() - last_winch > _link.rtt * 25: + # send window change, but rate limited (skip in no-tty mode) + if not no_tty and winch and time.time() - last_winch > _link.rtt * 25: last_winch = time.time() winch = False with contextlib.suppress(Exception): diff --git a/rnsh/rnsh.py b/rnsh/rnsh.py index c1e8486..8cf5c5d 100644 --- a/rnsh/rnsh.py +++ b/rnsh/rnsh.py @@ -146,7 +146,8 @@ async def _rnsh_cli_main(): noid=args.no_id, destination=args.destination, timeout=args.timeout, - command=args.command_line + command=args.command_line, + no_tty=args.no_tty ) return return_code if args.mirror else 0 else: From d197b9dacc99fca90a668a77eb06916554c11e4a Mon Sep 17 00:00:00 2001 From: Ian Blenke Date: Wed, 12 Nov 2025 16:57:41 +0000 Subject: [PATCH 2/3] Invert the double negative --- rnsh/initiator.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/rnsh/initiator.py b/rnsh/initiator.py index 3c3c366..ccf461c 100644 --- a/rnsh/initiator.py +++ b/rnsh/initiator.py @@ -233,9 +233,10 @@ async def initiate(configdir: str, identitypath: str, verbosity: int, quietness: loop = asyncio.get_running_loop() state = InitiatorState.IS_INITIAL # Determine pipe/TTY mode: force pipe if no_tty, otherwise auto-detect - is_stdin_pipe = no_tty or not os.isatty(sys.stdin.fileno()) - is_stdout_pipe = no_tty or not os.isatty(sys.stdout.fileno()) - is_stderr_pipe = no_tty or not os.isatty(sys.stderr.fileno()) + use_tty = not no_tty + is_stdin_pipe = not use_tty or not os.isatty(sys.stdin.fileno()) + is_stdout_pipe = not use_tty or not os.isatty(sys.stdout.fileno()) + is_stderr_pipe = not use_tty or not os.isatty(sys.stderr.fileno()) data_buffer = bytearray(sys.stdin.buffer.read()) if is_stdin_pipe else bytearray() line_buffer = bytearray() @@ -318,7 +319,7 @@ def stdin(): in_data = process.tty_read(sys.stdin.fileno()) if in_data is not None: # In no-tty mode, skip escape sequence processing - if no_tty: + if not use_tty: data_buffer.extend(in_data) else: data = bytearray() @@ -377,7 +378,7 @@ def stdin(): # Skip terminal attribute gathering in no-tty mode tcattr = None rows, cols, hpix, vpix = (None, None, None, None) - if not no_tty: + if use_tty: try: tcattr = termios.tcgetattr(0) rows, cols, hpix, vpix = process.tty_get_winsize(0) @@ -405,7 +406,7 @@ def stdin(): vpix=vpix)) # Skip window resize handling in no-tty mode - if not no_tty: + if use_tty: loop.add_signal_handler(signal.SIGWINCH, sigwinch_handler) _finished = asyncio.Event() loop.add_signal_handler(signal.SIGINT, functools.partial(_sigint_handler, signal.SIGINT, loop)) @@ -424,7 +425,7 @@ def stdin(): if isinstance(message, protocol.StreamDataMessage): if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT: if message.data and len(message.data) > 0: - if not no_tty: + if use_tty: ttyRestorer.raw() log.debug(f"stdout: {message.data}") os.write(1, message.data) @@ -433,7 +434,7 @@ def stdin(): os.close(1) if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDERR: if message.data and len(message.data) > 0: - if not no_tty: + if use_tty: ttyRestorer.raw() log.debug(f"stdout: {message.data}") os.write(2, message.data) @@ -495,7 +496,7 @@ def compress_adaptive(buf: bytes): processed = True # send window change, but rate limited (skip in no-tty mode) - if not no_tty and winch and time.time() - last_winch > _link.rtt * 25: + if use_tty and winch and time.time() - last_winch > _link.rtt * 25: last_winch = time.time() winch = False with contextlib.suppress(Exception): From 4a562b34eed1a12a590bd6065ff34e0b1d9f01df Mon Sep 17 00:00:00 2001 From: Ian Blenke Date: Wed, 12 Nov 2025 16:59:04 +0000 Subject: [PATCH 3/3] Update documentation --- rnsh/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rnsh/args.py b/rnsh/args.py index 2e2d0c9..c7868aa 100644 --- a/rnsh/args.py +++ b/rnsh/args.py @@ -49,7 +49,7 @@ def _split_array_at(arr: [_T], at: _T) -> ([_T], [_T]): -C --no-remote-command Disable executing command line from remote -m --mirror Client returns with code of remote process -w TIME --timeout TIME Specify client connect and request timeout in seconds - -T --no-tty Force pipe mode (no TTY); useful for ProxyCommand + -T --no-tty Force pipe mode (no TTY); useful for ssh ProxyCommand -q --quiet Increase quietness (move level up), multiple increases effect DEFAULT LOGGING LEVEL CRITICAL (silent)