Pseudo Terminal (PTY)
Interact with pseudo terminal sessions in your sandboxes.
Daytona provides pseudo terminal (PTY) capabilities through the process module in sandboxes. A PTY is a process started with the PTY kind: it runs under a virtual terminal so programs behave as if connected to a real terminal, with terminal size, line discipline, and interactive prompts.
PTY processes are exposed through a sandbox's PTY namespace and share the handle API of other processes, described in process execution.
Use cases
PTY sessions are used when a program expects a real terminal.
- Interactive development: REPLs, debuggers, and editors that read keystrokes and redraw the screen.
- Terminals in web applications: a browser terminal connects to a PTY session, sends keystrokes, and resizes the session with the window.
- Builds and tests: compilers, test runners, and deploy tools that show progress or colors only when attached to a terminal.
- System administration: setup tools and package managers that ask questions before they continue.
- Interrupts: a long-running process receives Ctrl+C through the terminal and stops cleanly.
- Reconnects: a client that reconnects replays the output from a cursor and sees what it missed.
- Shared sessions: a named session is reachable by its name from another client. Clients attached to one session at the same time share its shell, keystrokes, and output.
- Terminal agents: an agent works in one interactive shell that keeps its working directory and variables between commands. It reads the output as a terminal shows it, including escape sequences.
Start PTY session
Start a PTY session. Without a command, the session runs an interactive shell: the login shell of the user it runs as, or the first of bash, zsh, and sh found in the image. With a command, the session runs that command in the terminal instead. The terminal starts at 80 columns by 24 rows unless you set a size, and you can resize it later.
The SDKs and the CLI keep an exited session's record and output until deleted.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
# Without a command the PTY runs an interactive shell; the terminal defaults to 80x24
handle = sandbox.pty.start()
print(handle.id)from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start("python3 -i", cols=100, rows=40)| Parameter | Type | Description |
|---|---|---|
command | str | Sets the command to run in the PTY. |
cols | int | Sets the number of terminal columns. The value must be at least 1. The default is 80. |
rows | int | Sets the number of terminal rows. The value must be at least 1. The default is 24. |
cwd | str | Sets the working directory of the command. |
env | Dict[str, str] | Sets environment variables for the command. |
name | str | Sets the session name. See name PTY session. |
Name PTY session
Name a PTY session. A name aliases the session ID, so another client can reach the session without knowing its ID. The name belongs to the session only while it runs: when the session exits, the name is released for reuse, and the session stays reachable by its ID only.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start(name="dev-shell")
# Names are unique among live processes and resolve wherever an ID is accepted
info = sandbox.process.get("dev-shell")Configure shell, user, and timeout
Configure the shell, the user, and the timeout of a PTY session. A shell that cannot be found or a user that does not exist fails the start. When the timeout elapses, the session is killed and reports that it timed out. The Python SDK does not set these options for PTY sessions.
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.v2.create()
const handle = await sandbox.pty.start(undefined, {
shell: '/bin/bash', // default: the user's login shell, then bash, zsh, sh
user: 'root', // run as another user
executionTimeout: 600_000, // SIGKILL after 10 minutes; wait() reports reason 'timed_out'
})| Parameter | Type | Description |
|---|---|---|
shell | string | Sets the shell, as a path or a name looked up on PATH. |
user | string | Sets the user the session runs as. The user must exist in /etc/passwd of the sandbox. |
Connect to PTY session
Connect to an existing PTY session by its ID. Connecting creates a handle without contacting the sandbox; the handle sends input to the session, reads its output, resizes it, and stops it. A live connection streams output produced after it opens; to see earlier output, replay it from a cursor.
handle = sandbox.pty.connect(process_id)
handle.stdin('echo "Reconnected"\n')
# The Python SDK has no WebSocket attach; handle.attach() raises
# UnsupportedOperationError. Use stdin() and stream_logs() instead.List PTY sessions
List PTY sessions, including retained exited ones; check each session's state.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
processes = sandbox.pty.list()
for info in processes:
print(f"PTY: {info.id} ({info.state})")
if info.terminal:
print(f" Size: {info.terminal.cols}x{info.terminal.rows}")
print(f" Working directory: {info.cwd}")Get PTY session info
info = handle.get()
print(f"PTY: {info.id}")
print(f"State: {info.state}")
print(f"PID: {info.pid}")
if info.terminal:
print(f"Size: {info.terminal.cols}x{info.terminal.rows}")
print(f"Working directory: {info.cwd}")
if info.state == "terminal":
print(f"Exit code: {info.exit_code}")Kill PTY session
Stop a PTY session and remove it. Stopping sends a signal to the session's processes; removing deletes the session record and its output. Removing a session that is still running kills it first.
# Sends SIGTERM; cleanup() removes the process record and logs
handle.kill()
handle.cleanup()| Parameter | Type | Description |
|---|---|---|
signal | SignalName | Sets the signal that kill() sends: TERM, KILL, INT, HUP, QUIT, USR1, or USR2. The default is "TERM". |
escalate_after | float | Sends escalate_to if the session is still running after this delay. The delay is in seconds, at most 600. |
escalate_to | SignalName | Sets the signal sent after escalate_after. The default is "KILL". |
Delete a PTY session
Delete a PTY session in one step: a running session gets a termination signal, then the session record and its output are removed. Deleting a non-PTY process fails and leaves it running.
sandbox.pty.delete(handle.id)Resize PTY session
Resize the terminal of a running PTY session. The program in the terminal receives a window size change signal and can redraw at the new size. The session record reports the new size.
handle.resize(cols=150, rows=40)Send input
Send input to a PTY session. Input reaches the terminal as if typed, so end each command with a newline. Control keys are bytes: send Ctrl+C as 0x03 and Ctrl+D as 0x04. A terminal has no separate end of input; to end input, send Ctrl+D.
handle.stdin("ls -la\n")| Parameter | Type | Description |
|---|---|---|
data | Union[str, bytes] | Sets the input to send, as str or bytes. Bytes are sent base64 encoded. This parameter is required. |
Read output
Read one page of PTY output. A page holds up to 1,000 frames by default. To read past the first page, continue from a cursor as shown in replay output from a cursor.
page = handle.logs()
for frame in page.items:
print(frame.data, end="")| Parameter | Type | Description |
|---|---|---|
limit | int | Sets the page size. The daemon clamps the page size to 1 to 5000. The default is 1000. |
encoding | LogEncoding | Sets the output encoding. In text encoding, bytes that are not valid UTF-8 are replaced. Pass "base64" to read raw bytes. The default is "text". |
Replay output from a cursor
Replay PTY output from a cursor. Every output frame carries a cursor, and a read with a cursor returns the frames after it, so a client that saved the last cursor it processed can resume without gaps or duplicates. Retention can evict old output; a cursor that points into evicted output fails, and the error carries the oldest cursor still available. See log retention.
# Every frame carries a cursor; reading resumes strictly after it. An empty page means the read has caught up
cursor = last_cursor
while True:
page = handle.logs(cursor=cursor)
if not page.items:
break
for frame in page.items:
print(frame.data, end="")
cursor = frame.cursorWait for completion
Wait for a PTY session to exit. The result holds the exit code, the ending signal if any, and a reason: exited, killed by a signal, timed out, failed to start, or lost. If the wait time elapses first, the result reports a timeout with no exit code, and the session keeps running.
result = handle.wait(timeout=30)
print(result.reason, result.exit_code)| Parameter | Type | Description |
|---|---|---|
timeout | float | Sets how long to wait, in seconds. Omit it or pass 0 to wait until the session exits. |
Interactive shell session
Run several commands in one interactive shell. The shell keeps its state between commands, such as the working directory and variables. Read output while you send input; the output stream ends after the shell exits and all of its output is delivered.
import threading
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start()
# Print output in the background
def pump():
for frame in handle.stream_logs():
print(frame.data, end="")
reader = threading.Thread(target=pump, daemon=True)
reader.start()
for command in [
"pwd",
"ls -la",
"echo 'Hello from PTY'",
"python3 -c 'print(2 + 2)'",
"exit",
]:
handle.stdin(command + "\n")
result = handle.wait()
reader.join(timeout=5)
print(f"\nShell exited with code: {result.exit_code}")
handle.cleanup()Long-running process with interrupt
Interrupt a running command from the terminal. Ctrl+C sent as input goes through the terminal, which interrupts the foreground command; the shell itself keeps running. Signaling the session instead targets the whole session, including the shell. See kill PTY session.
import time
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start()
handle.stdin("for i in $(seq 1 100); do echo \"Line $i\"; sleep 1; done\n")
time.sleep(5)
handle.stdin("\x03") # Ctrl+C
handle.stdin("exit\n")
result = handle.wait()
for frame in handle.logs().items:
print(frame.data, end="")
print(f"\nExit code: {result.exit_code}")
handle.cleanup()Resource management
Remove a PTY session when you no longer need it. The SDKs keep an exited session's record and output until cleanup, so clean up in a final block to remove it even when a step fails.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start()
try:
handle.stdin("echo hello\n")
handle.stdin("exit\n")
handle.wait(timeout=10)
finally:
handle.cleanup()