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
39 changes: 32 additions & 7 deletions remote_developer/components/connectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
92 changes: 91 additions & 1 deletion remote_developer/components/execution.py
Original file line number Diff line number Diff line change
@@ -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())

Expand Down Expand Up @@ -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)
26 changes: 14 additions & 12 deletions remote_developer/remote_developer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
Loading