From f767b356827192cede0aff866232e07381a43e7e Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 12 Jul 2022 15:54:07 +0200 Subject: [PATCH 1/4] channel: Implement new escape sequence Let's follow systemd and use "CTRL-] three times within 1 second" as the universal escape sequence in tbot. There are a number of reasons for this: 1. The old `~.` would conflict with ssh sessions which means you need to escape the escape sequence (= `~~.`) when trying to exit a tbot interactive session inside an ssh session. 2. Any key-sequence made up of commonly used characters is bound to conflict _somewhere_. CTRL-] will most likely never show up in actual interaction and even less so as three consecutive presses. Channel.attach_interactive() will now also print out a message instructing about this escape sequence. This should make sure nobody will ever get stuck inside an interactive session again. The old "CTRL-D to exit" behavior can be re-activated for specific usecases by setting the `ctrld_exit` parameter. Signed-off-by: Harald Seiler --- tbot/machine/channel/channel.py | 47 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/tbot/machine/channel/channel.py b/tbot/machine/channel/channel.py index 912ec3a9..755a444a 100644 --- a/tbot/machine/channel/channel.py +++ b/tbot/machine/channel/channel.py @@ -1010,7 +1010,7 @@ def take(self) -> "Channel": # interactive {{{ def attach_interactive( - self, end_magic: typing.Union[str, bytes, None] = None + self, end_magic: typing.Union[str, bytes, None] = None, ctrld_exit: bool = False ) -> None: """ Connect tbot's terminal to this channel. @@ -1018,9 +1018,21 @@ def attach_interactive( Allows the user to interact directly with whatever this channel is connected to. - :param str, bytes end_magic: String that, when detected, should end the - interactive session. If no ``end_magic`` is given, pressing - ``CTRL-D`` will terminate the session. + The interactive session can be exited at any point by **pressing** + ``CTRL+]`` **three times within 1 second**. + + :param str, bytes end_magic: The ``end_magic`` parameter may be used to + define an automatic exit condition (sequence sent from the remote + side to trigger the end). + :param bool ctrld_exit: If ``True``, pressing ``CTRL-D`` will also + terminate the session immediately. + + .. versionchanged:: UNRELEASED + + - The escape sequence is now "Press ``CTRL-]`` three times within 1 + second". + - The ``ctrld_exit`` parameter was added to restore the old + "``CTRL-D`` to exit" behavior. """ end_magic_bytes = ( end_magic.encode("utf-8") if isinstance(end_magic, str) else end_magic @@ -1034,8 +1046,14 @@ def attach_interactive( old_blacklist = self._write_blacklist self._write_blacklist = [] + escape_timestamp = None previous: typing.Deque[int] = collections.deque(maxlen=3) + if not ctrld_exit: + tbot.log.message( + tbot.log.c("Press CTRL+] three times within 1 second to exit.").bold + ) + oldtty = termios.tcgetattr(sys.stdin) try: tty.setraw(sys.stdin.fileno()) @@ -1067,13 +1085,22 @@ def attach_interactive( if sys.stdin in r: data = sys.stdin.buffer.read(4096) previous.extend(data) - if end_magic is None and data == b"\x04": - break - for a, b in itertools.zip_longest(previous, b"\r~."): - if a != b: - break - else: + + # Old ^D to exit behavior + if ctrld_exit and data == b"\x04": break + + # Detect whether ^] was pressed 3 times in 1 second + now = time.monotonic() + if escape_timestamp is None and 0x1D in previous: + escape_timestamp = now + elif escape_timestamp is not None: + if now < escape_timestamp + 1: + if previous.count(0x1D) >= 3: + break + else: + escape_timestamp = None + self.send(data) sys.stdout.write("\r\n") From 24ad35f9ebe3768a8dc510e5cfd50df96246c363 Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 12 Jul 2022 16:11:25 +0200 Subject: [PATCH 2/4] machine.linux: Fix interactive escape behavior The re-acquisition of the shell needs to deal with the system still being in the innermost "user shell". This can happen when the interactive session is exited by pressing the escape sequence. Signed-off-by: Harald Seiler --- tbot/machine/linux/ash.py | 7 ++++++- tbot/machine/linux/bash.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tbot/machine/linux/ash.py b/tbot/machine/linux/ash.py index 66886d51..d73b6a8f 100644 --- a/tbot/machine/linux/ash.py +++ b/tbot/machine/linux/ash.py @@ -268,6 +268,11 @@ def interactive(self) -> None: try: self.ch.sendline("exit") - self.ch.read_until_prompt(timeout=0.5) + try: + self.ch.read_until_prompt(timeout=0.5) + except TimeoutError: + # we might still be in the inner shell so let's try exiting again + self.ch.sendline("exit") + self.ch.read_until_prompt(timeout=0.5) except TimeoutError: raise Exception("Failed to reacquire shell after interactive session!") diff --git a/tbot/machine/linux/bash.py b/tbot/machine/linux/bash.py index 5cd5014d..2db3c0ce 100644 --- a/tbot/machine/linux/bash.py +++ b/tbot/machine/linux/bash.py @@ -276,6 +276,11 @@ def interactive(self) -> None: try: self.ch.sendline("exit") - self.ch.read_until_prompt(timeout=0.5) + try: + self.ch.read_until_prompt(timeout=0.5) + except TimeoutError: + # we might still be in the inner shell so let's try exiting again + self.ch.sendline("exit") + self.ch.read_until_prompt(timeout=0.5) except TimeoutError: raise Exception("Failed to reacquire shell after interactive session!") From eac9bbafccd1820461d8334257a3289a2563b771 Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 12 Jul 2022 16:18:34 +0200 Subject: [PATCH 3/4] machine: Update interactive session messages Drop the exit hint as that is now printed by attach_interactive() already. Signed-off-by: Harald Seiler --- tbot/machine/board/uboot.py | 4 +--- tbot/machine/shell.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tbot/machine/board/uboot.py b/tbot/machine/board/uboot.py index 0ddf5128..85203462 100644 --- a/tbot/machine/board/uboot.py +++ b/tbot/machine/board/uboot.py @@ -380,9 +380,7 @@ def interactive(self) -> None: can interactively run commands. This method is used by the ``interactive_uboot`` testcase. """ - tbot.log.message( - f"Entering interactive shell ({tbot.log.c('CTRL+D to exit').bold}) ..." - ) + tbot.log.message(f"Entering interactive shell...") # It is important to send a space before the newline. Otherwise U-Boot # will reexecute the last command which we definitely do not want here. diff --git a/tbot/machine/shell.py b/tbot/machine/shell.py index 6bc0fb28..fc990a12 100644 --- a/tbot/machine/shell.py +++ b/tbot/machine/shell.py @@ -108,7 +108,5 @@ def interactive(self) -> None: Connect tbot's stdio to this machine's channel. This will allow interactive access to the machine. """ - tbot.log.message( - f"Entering interactive shell ({tbot.log.c('CTRL+D to exit').bold}) ..." - ) + tbot.log.message(f"Entering interactive shell...") self.ch.attach_interactive() From 3d95db8d33045280ff9eaeae404823c46cfac86f Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 12 Jul 2022 16:19:22 +0200 Subject: [PATCH 4/4] contrib: gdb: Update interactive gdb session This one should still use CTRL-D to exit behavior so explicitly re-enable it here. Signed-off-by: Harald Seiler --- tbot_contrib/gdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tbot_contrib/gdb.py b/tbot_contrib/gdb.py index 17f6af58..2711fd04 100644 --- a/tbot_contrib/gdb.py +++ b/tbot_contrib/gdb.py @@ -103,7 +103,7 @@ def interactive(self) -> None: tbot.log.message( f"Entering interactive GDB shell ({tbot.log.c('CTRL+D to exit').bold}) ..." ) - self.ch.attach_interactive() + self.ch.attach_interactive(ctrld_exit=True) self.ch.sendcontrol("C") self.ch.read_until_prompt("(gdb) ")