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/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") 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!") 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() 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) ")