Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion rnsh/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def _split_array_at(arr: [_T], at: _T) -> ([_T], [_T]):
[-b <period>] [-n] [-a <identity_hash>] ([-a <identity_hash>] ...) [-A | -C]
[[--] <program> [<arg> ...]]
rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] -p
rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] [-N] [-m] [-w <timeout>]
rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] [-N] [-m] [-w <timeout>] [-T]
<destination_hash> [[--] <program> [<arg> ...]]
rnsh -h
rnsh --version
Expand All @@ -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 ssh ProxyCommand
-q --quiet Increase quietness (move level up), multiple increases effect
DEFAULT LOGGING LEVEL
CRITICAL (silent)
Expand Down Expand Up @@ -106,6 +107,7 @@ def __init__(self, argv: [str]):
self.program_args = args.get("<arg>", 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:
Expand Down
139 changes: 77 additions & 62 deletions rnsh/initiator.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,18 @@ 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
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()

await _initiate_link(
Expand Down Expand Up @@ -313,88 +318,96 @@ 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 not use_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 use_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,
cols=cols,
hpix=hpix,
vpix=vpix))

loop.add_signal_handler(signal.SIGWINCH, sigwinch_handler)
# Skip window resize handling in no-tty mode
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))
loop.add_signal_handler(signal.SIGTERM, functools.partial(_sigint_handler, signal.SIGTERM, loop))
Expand All @@ -412,15 +425,17 @@ 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 use_tty:
ttyRestorer.raw()
log.debug(f"stdout: {message.data}")
os.write(1, message.data)
sys.stdout.flush()
if message.eof:
os.close(1)
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDERR:
if message.data and len(message.data) > 0:
ttyRestorer.raw()
if use_tty:
ttyRestorer.raw()
log.debug(f"stdout: {message.data}")
os.write(2, message.data)
sys.stderr.flush()
Expand Down Expand Up @@ -480,8 +495,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 use_tty and winch and time.time() - last_winch > _link.rtt * 25:
last_winch = time.time()
winch = False
with contextlib.suppress(Exception):
Expand Down
3 changes: 2 additions & 1 deletion rnsh/rnsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down