diff --git a/remote_developer/components/connectivity.py b/remote_developer/components/connectivity.py index a97ba75..73f6965 100644 --- a/remote_developer/components/connectivity.py +++ b/remote_developer/components/connectivity.py @@ -6,22 +6,47 @@ async def execute_remote_command(ssh_client, command): - """Executes a command on the remote server using the SSH client.""" + """Executes a command on the remote host.""" + if ( + not ssh_client + or not ssh_client.get_transport() + or not ssh_client.get_transport().is_active() + ): + logger.error("SSH connection is not active") + return None + try: logger.debug(f"Executing remote command: {command}") stdin, stdout, stderr = ssh_client.exec_command(command) - output = stdout.read().decode("utf-8") - error = stderr.read().decode("utf-8") - if output: - logger.debug(f"Command output: {output.strip()}") - if error: - logger.error(f"Command error: {error.strip()}") + output = stdout.read().decode().strip() + error = stderr.read().decode().strip() + return output, error except Exception as e: logger.error(f"Error executing command: {e}") return None, str(e) +async def execute_interactive_command(ssh_client, command): + """Executes a command with interactive PTY session.""" + if ( + not ssh_client + or not ssh_client.get_transport() + or not ssh_client.get_transport().is_active() + ): + logger.error("SSH connection is not active") + return None + + try: + channel = ssh_client.get_transport().open_session() + channel.get_pty() + channel.exec_command(command) + return channel + except Exception as e: + logger.error(f"Error executing interactive command: {e}") + return None + + async def ensure_remote_directory(config, ssh_client): """Ensures that the remote directory exists.""" remote_host = config["remote_host"] diff --git a/remote_developer/components/execution.py b/remote_developer/components/execution.py index ff681d1..7952a50 100644 --- a/remote_developer/components/execution.py +++ b/remote_developer/components/execution.py @@ -1,10 +1,12 @@ """Execution module.""" from utils.logger import Logger, get_level_from_env -from components.connectivity import execute_remote_command +from components.connectivity import execute_remote_command, execute_interactive_command import posixpath import os from utils import regcache +import sys +import select logger = Logger(__name__, level=get_level_from_env()) @@ -45,3 +47,91 @@ async def run_command_in_devcontainer(config, ssh_client, command_parts): logger.error( f"An unexpected error occurred while running the command in the devcontainer: {e}" ) + + +async def execute_container_shell(config, ssh_client): + """Opens an interactive shell session in the running devcontainer. + + Args: + config (dict): Configuration dictionary containing remote directory and container name. + ssh_client (paramiko.SSHClient): SSH client object for connecting to the remote server. + """ + remote_host = config["remote_host"] + connection_info = regcache.get_cache_item(remote_host, CACHE_LOCATION) or {} + container_id = connection_info.get("container_id") + + if not container_id: + logger.error("No container ID found in cache") + return None + + docker_shell_command = ( + f'docker exec -it $(docker ps -q --filter "name={container_id}" | head -n 1) bash' + ) + + try: + logger.debug("Opening interactive shell in devcontainer") + channel = await execute_interactive_command(ssh_client, docker_shell_command) + + if channel is None: + return + + try: + # Windows doesn't support termios, so we need a different approach + if os.name == "nt": + import msvcrt + + while True: + # Check if there's data to read from the channel + if channel.recv_ready(): + data = channel.recv(1024) + if len(data) == 0: + break + sys.stdout.buffer.write(data) + sys.stdout.buffer.flush() + + # Check if there's input from the user + if msvcrt.kbhit(): + char = msvcrt.getch() + channel.send(char) + + # Add a small sleep to prevent high CPU usage + import time + + time.sleep(0.01) + else: + # Unix-like systems can use termios + import termios + import tty + + old_tty = termios.tcgetattr(sys.stdin) + try: + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + channel.settimeout(0.0) + + while True: + r, w, e = select.select([channel, sys.stdin], [], []) + if channel in r: + try: + data = channel.recv(1024) + if len(data) == 0: + break + sys.stdout.buffer.write(data) + sys.stdout.buffer.flush() + except Exception: + break + if sys.stdin in r: + x = sys.stdin.read(1) + if len(x) == 0: + break + channel.send(x) + finally: + # Restore terminal settings on Unix-like systems + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) + + finally: + channel.close() + + except Exception as e: + error_msg = f"An unexpected error occurred while opening the shell in the devcontainer: {e}" + logger.error(error_msg) diff --git a/remote_developer/remote_developer.py b/remote_developer/remote_developer.py index 103abb5..1a70130 100644 --- a/remote_developer/remote_developer.py +++ b/remote_developer/remote_developer.py @@ -66,28 +66,30 @@ async def setup_and_connect(ctx): async def run_rdc_command(ctx, ssh_client, command): - """Runs a command in the devcontainer.""" + """Runs a command in the remote devcontainer.""" try: await execution.run_command_in_devcontainer(ctx.obj["config"], ssh_client, command) except Exception as e: logger.error(f"An error occurred while running the command: {e}") +async def open_shell(ctx, ssh_client): + """Opens a shell in the remote devcontainer.""" + try: + await execution.execute_container_shell(ctx.obj["config"], ssh_client) + except Exception as e: + logger.error(f"An error occurred while starting the shell: {e}") + + async def interactive_shell(ctx, ssh_client): """Opens an interactive shell to the devcontainer.""" logger.info("Entering interactive shell. Type 'exit' to quit.") try: - while True: - try: - command = input("[remote-dev]> ") - if command.lower() == "exit": - break - await run_rdc_command(ctx, ssh_client, command.split()) - except KeyboardInterrupt: - logger.debug("\nExiting interactive shell.") - break - except Exception as e: - logger.error(f"Error running command: {e}") + await open_shell(ctx, ssh_client) + except KeyboardInterrupt: + logger.debug("\nExiting interactive shell.") + except Exception as e: + logger.error(f"Error running interactive shell: {e}") finally: try: await security.close_ssh_connection(ssh_client, ctx.obj["config"]["remote_host"])