Interactive Sessions
TL;DR
Create SSH-like interactive terminal sessions with full PTY support using stream.attach() and stream.send_input(). Sessions persist across disconnections, allowing you to detach and reattach later. Multiple clients can attach simultaneously for pair programming or monitoring. Use observer mode for read-only access. Supports interactive programs like vim, htop, and debuggers via escape sequences.
Full PTY access for interactive commands like vim, htop, or shells.
How do I start a basic interactive session?
from cmdop import AsyncCMDOPClient
import asyncio
async def main():
async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
# Retrieve the active terminal session for the specified machine
session = await client.terminal.get_active_session("my-server")
# Create a new stream object for bidirectional communication
stream = client.terminal.stream()
# Register a callback to print output as it arrives in real-time
stream.on_output(lambda data: print(data.decode(), end="", flush=True))
# Attach the stream to the remote session (starts receiving output)
await stream.attach(session.session_id)
# Send a command to the remote terminal as raw bytes
await stream.send_input(b"ls -la\n")
# Wait briefly for the output to arrive over the network
await asyncio.sleep(2)
# Detach from the session (the remote session continues running)
await stream.close()
asyncio.run(main())How do I get full terminal emulation with keyboard input?
For a complete terminal experience with keyboard input:
import sys
import tty
import termios
import asyncio
from cmdop import AsyncCMDOPClient
async def interactive_terminal():
async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
session = await client.terminal.get_active_session("my-server")
stream = client.terminal.stream()
# Write raw bytes directly to stdout for accurate terminal rendering
stream.on_output(lambda data: sys.stdout.buffer.write(data) or sys.stdout.flush())
await stream.attach(session.session_id)
# Save current terminal settings so we can restore them later
old_settings = termios.tcgetattr(sys.stdin)
try:
# Switch to raw mode: each keypress is sent immediately (no line buffering)
tty.setraw(sys.stdin.fileno())
# Read one character at a time and forward to the remote session
while True:
char = sys.stdin.read(1)
if char == '\x03': # Ctrl+C to exit the local client
break
await stream.send_input(char.encode())
finally:
# Restore the original terminal settings on exit
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
await stream.close()
asyncio.run(interactive_terminal())How do I control interactive programs like vim?
CMDOP supports full PTY, so interactive programs work:
# Launch vim to edit a file on the remote machine
await stream.send_input(b"vim file.txt\n")
# Send arrow key escape sequences for navigation
await stream.send_input(b"\x1b[A") # Up arrow
await stream.send_input(b"\x1b[B") # Down arrow
# Force-quit vim without saving
await stream.send_input(b":q!\n")What are the common escape sequences for terminal input?
| Key | Escape Sequence |
|---|---|
| Up | \x1b[A |
| Down | \x1b[B |
| Right | \x1b[C |
| Left | \x1b[D |
| Home | \x1b[H |
| End | \x1b[F |
| Page Up | \x1b[5~ |
| Page Down | \x1b[6~ |
| Tab | \t |
| Enter | \n or \r |
| Backspace | \x7f |
| Ctrl+C | \x03 |
| Ctrl+D | \x04 |
| Ctrl+Z | \x1a |
How do sessions persist across disconnections?
The key feature: session survives disconnection.
# Client A starts a long-running process and then disconnects
await stream.send_input(b"./long_running_task.sh\n")
await stream.close() # Disconnect from the session (process keeps running)
# ... hours later ...
# Client B reconnects to the same session and sees ongoing output
session = await client.terminal.get_active_session("my-server")
stream = client.terminal.stream()
await stream.attach(session.session_id)
# Process still running! Output continues from where Client A left off.How do multiple clients connect to the same session?
Two clients can attach simultaneously:
# Client A attaches to the session as an operator (read + write)
stream_a = client.terminal.stream()
await stream_a.attach(session.session_id)
# Client B also attaches to the same session as an operator
stream_b = client.terminal.stream()
await stream_b.attach(session.session_id)
# Both see the same output in real-time
# Both can send input (shared terminal)How do I use observer mode for read-only access?
Read-only access:
# Create a stream in observer mode (read-only, no input allowed)
stream = client.terminal.stream(mode="observer")
await stream.attach(session.session_id)
# Output is visible to the observer
stream.on_output(print_output)
# Attempting to send input in observer mode raises an error:
# await stream.send_input(b"command") # PermissionErrorHow do I handle stream events?
stream = client.terminal.stream()
# Called whenever the remote session produces output
stream.on_output(lambda data: process_output(data))
# Called when the WebSocket connection to the server drops
stream.on_disconnect(lambda: print("Disconnected"))
# Called when the connection is automatically re-established
stream.on_reconnect(lambda: print("Reconnected"))
# Called when an error occurs during streaming
stream.on_error(lambda e: print(f"Error: {e}"))
await stream.attach(session.session_id)What are common use cases for interactive sessions?
How do I monitor logs in real-time?
async def monitor_logs():
session = await client.terminal.get_active_session("prod-server")
# Use observer mode since we only need to read log output
stream = client.terminal.stream(mode="observer")
async def on_output(data):
text = data.decode()
# Check each output chunk for error patterns and send alerts
if "ERROR" in text:
send_alert(text)
stream.on_output(on_output)
await stream.attach(session.session_id)
# Start tailing the application log file continuously
await stream.send_input(b"tail -f /var/log/app.log\n")
# Block forever to keep receiving log output
await asyncio.Event().wait()How do I debug remotely with pdb?
# Launch the Python debugger on the remote machine
await stream.send_input(b"python -m pdb script.py\n")
# Send debugger commands interactively
await stream.send_input(b"n\n") # next: execute the next line
await stream.send_input(b"s\n") # step: step into a function call
await stream.send_input(b"p variable\n") # print: inspect a variable's value
await stream.send_input(b"c\n") # continue: resume execution until next breakpointHow do I set up pair programming?
# Developer A shares their session ID with Developer B
session = await client.terminal.get_active_session("dev-machine")
print(f"Share this: {session.session_id}")
# Developer B attaches to the shared session using the session ID
await other_stream.attach(shared_session_id)
# Both developers see the same terminal and can type commands!What are the best practices for interactive sessions?
How do I properly close streams?
try:
await stream.attach(session.session_id)
# ... work with the session ...
finally:
# Always close the stream in a finally block to avoid resource leaks
await stream.close()How do I handle disconnections gracefully?
# Register handlers to react to network interruptions automatically
stream.on_disconnect(handle_disconnect)
stream.on_reconnect(handle_reconnect)When should I use observer mode?
# Use observer mode when you only need to watch (e.g., monitoring, logging)
stream = client.terminal.stream(mode="observer")Next
Last updated on