Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/pty-handle-send-stdin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@e2b/python-sdk": patch
"@e2b/cli": patch
"e2b": patch
---

Support sending input to a PTY directly through the command handle (`handle.send_stdin` / `handle.sendStdin`), matching the regular command handle. Previously stdin could only be sent via the PID-keyed `sandbox.pty.send_stdin` / `sandbox.pty.sendInput`, and calling `handle.send_stdin` raised "Sending stdin is not supported for this command handle." PTY input methods now also accept `str` input, encoding it as UTF-8.

In the JS SDK, `sandbox.pty.sendStdin()` is added as the canonical PTY input method to mirror `sandbox.commands.sendStdin()`; the existing `sandbox.pty.sendInput()` remains as a deprecated alias. The CLI is migrated onto `sendStdin()` internally (no behavior change).
2 changes: 1 addition & 1 deletion packages/cli/src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) {

const inputQueue = new BatchedQueue<Buffer>(async (batch) => {
const combined = Buffer.concat(batch)
await sandbox.pty.sendInput(terminalSession.pid, combined)
await sandbox.pty.sendStdin(terminalSession.pid, combined)
}, FLUSH_INPUT_INTERVAL_MS)

const resizeListener = process.stdout.on('resize', () =>
Expand Down
30 changes: 25 additions & 5 deletions packages/js-sdk/src/sandbox/commands/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class Pty {
undefined,
undefined,
opts.onData,
undefined,
(data, stdinOpts) => this.sendStdin(pid, data, stdinOpts),
undefined,
this.checkHealth
)
Expand Down Expand Up @@ -210,7 +210,7 @@ export class Pty {
undefined,
undefined,
opts?.onData,
undefined,
(data, stdinOpts) => this.sendStdin(pid, data, stdinOpts),
undefined,
this.checkHealth
)
Expand All @@ -227,9 +227,9 @@ export class Pty {
* @param data input data to send to the PTY.
* @param opts connection options.
*/
async sendInput(
async sendStdin(
pid: number,
data: Uint8Array,
data: string | Uint8Array,
opts?: Pick<ConnectionOpts, 'requestTimeoutMs' | 'signal'>
): Promise<void> {
try {
Expand All @@ -238,7 +238,10 @@ export class Pty {
input: {
input: {
case: 'pty',
value: data,
value:
typeof data === 'string'
? new TextEncoder().encode(data)
: data,
},
},
process: {
Expand All @@ -260,6 +263,23 @@ export class Pty {
}
}

/**
* Send input to a PTY.
*
* @deprecated Use {@link Pty.sendStdin} instead.
*
* @param pid process ID of the PTY.
* @param data input data to send to the PTY.
* @param opts connection options.
*/
async sendInput(
pid: number,
data: string | Uint8Array,
opts?: Pick<ConnectionOpts, 'requestTimeoutMs' | 'signal'>
): Promise<void> {
return this.sendStdin(pid, data, opts)
}

Comment thread
mishushakov marked this conversation as resolved.
/**
* Resize PTY.
* Call this when the terminal window is resized and the number of columns and rows has changed.
Expand Down
39 changes: 38 additions & 1 deletion packages/js-sdk/tests/sandbox/pty/sendInput.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { expect } from 'vitest'
import { sandboxTest } from '../../setup'

sandboxTest('send input', async ({ sandbox }) => {
sandboxTest('send stdin', async ({ sandbox }) => {
const terminal = await sandbox.pty.create({
cols: 80,
rows: 24,
onData: () => null,
})

await sandbox.pty.sendStdin(
terminal.pid,
new Uint8Array(Buffer.from('exit\n'))
)

await terminal.wait()
expect(terminal.exitCode).toBe(0)
})

sandboxTest('send input (deprecated alias)', async ({ sandbox }) => {
const terminal = await sandbox.pty.create({
cols: 80,
rows: 24,
Expand All @@ -16,3 +32,24 @@ sandboxTest('send input', async ({ sandbox }) => {
await terminal.wait()
expect(terminal.exitCode).toBe(0)
})

sandboxTest('send stdin through handle', async ({ sandbox }) => {
let output = ''

const terminal = await sandbox.pty.create({
cols: 80,
rows: 24,
envs: { ABC: '123' },
onData: (data) => {
output += new TextDecoder().decode(data)
},
})

// Send input directly through the handle instead of the PID-keyed module method.
await terminal.sendStdin(new Uint8Array(Buffer.from('echo $ABC\n')))
await terminal.sendStdin('exit\n')

await terminal.wait()
expect(terminal.exitCode).toBe(0)
expect(output).toContain('123')
})
22 changes: 15 additions & 7 deletions packages/python-sdk/e2b/sandbox_async/commands/pty.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Optional
from typing import Dict, Optional, Union

import e2b_connect
import httpcore
Expand Down Expand Up @@ -82,7 +82,7 @@ async def kill(
async def send_stdin(
self,
pid: int,
data: bytes,
data: Union[str, bytes],
request_timeout: Optional[float] = None,
) -> None:
"""
Expand All @@ -97,7 +97,7 @@ async def send_stdin(
process_pb2.SendInputRequest(
process=process_pb2.ProcessSelector(pid=pid),
input=process_pb2.ProcessInput(
pty=data,
pty=data.encode() if isinstance(data, str) else data,
),
),
request_timeout=self._connection_config.get_request_timeout(
Expand Down Expand Up @@ -164,11 +164,15 @@ async def create(
f"Failed to start process: expected start event, got {start_event}"
)

pid = start_event.event.start.pid
return AsyncCommandHandle(
pid=start_event.event.start.pid,
handle_kill=lambda: self.kill(start_event.event.start.pid),
pid=pid,
handle_kill=lambda: self.kill(pid),
events=events,
on_pty=on_data,
handle_send_stdin=lambda data, request_timeout=None: self.send_stdin(
pid, data, request_timeout
),
check_health=self._check_health,
)
except Exception as e:
Expand Down Expand Up @@ -216,11 +220,15 @@ async def connect(
f"Failed to connect to process: expected start event, got {start_event}"
)

pid = start_event.event.start.pid
return AsyncCommandHandle(
pid=start_event.event.start.pid,
handle_kill=lambda: self.kill(start_event.event.start.pid),
pid=pid,
handle_kill=lambda: self.kill(pid),
events=events,
on_pty=on_data,
handle_send_stdin=lambda data, request_timeout=None: self.send_stdin(
pid, data, request_timeout
),
check_health=self._check_health,
)
except Exception as e:
Expand Down
22 changes: 15 additions & 7 deletions packages/python-sdk/e2b/sandbox_sync/commands/pty.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import httpx
import threading

from typing import Dict, Optional
from typing import Dict, Optional, Union

from packaging.version import Version
from e2b.api import make_logging_event_hooks
Expand Down Expand Up @@ -110,7 +110,7 @@ def kill(
def send_stdin(
self,
pid: int,
data: bytes,
data: Union[str, bytes],
request_timeout: Optional[float] = None,
) -> None:
"""
Expand All @@ -125,7 +125,7 @@ def send_stdin(
process_pb2.SendInputRequest(
process=process_pb2.ProcessSelector(pid=pid),
input=process_pb2.ProcessInput(
pty=data,
pty=data.encode() if isinstance(data, str) else data,
),
),
request_timeout=self._connection_config.get_request_timeout(
Expand Down Expand Up @@ -190,10 +190,14 @@ def create(
f"Failed to start process: expected start event, got {start_event}"
)

pid = start_event.event.start.pid
return CommandHandle(
pid=start_event.event.start.pid,
handle_kill=lambda: self.kill(start_event.event.start.pid),
pid=pid,
handle_kill=lambda: self.kill(pid),
events=events,
handle_send_stdin=lambda data, request_timeout=None: self.send_stdin(
pid, data, request_timeout
),
check_health=self._check_health,
)
except Exception as e:
Expand Down Expand Up @@ -239,10 +243,14 @@ def connect(
f"Failed to connect to process: expected start event, got {start_event}"
)

pid = start_event.event.start.pid
return CommandHandle(
pid=start_event.event.start.pid,
handle_kill=lambda: self.kill(start_event.event.start.pid),
pid=pid,
handle_kill=lambda: self.kill(pid),
events=events,
handle_send_stdin=lambda data, request_timeout=None: self.send_stdin(
pid, data, request_timeout
),
check_health=self._check_health,
)
except Exception as e:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,21 @@ async def test_send_input(async_sandbox: AsyncSandbox):
await async_sandbox.pty.send_stdin(terminal.pid, b"exit\n")
await terminal.wait()
assert terminal.exit_code == 0


async def test_handle_send_stdin(async_sandbox: AsyncSandbox):
output = []

terminal = await async_sandbox.pty.create(
PtySize(cols=80, rows=24),
on_data=lambda x: output.append(x.decode("utf-8")),
envs={"ABC": "123"},
)

# Send input directly through the handle instead of the PID-keyed module method.
await terminal.send_stdin(b"echo $ABC\n")
await terminal.send_stdin("exit\n")

await terminal.wait()
assert terminal.exit_code == 0
assert "123" in "".join(output)
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,17 @@ def test_send_input(sandbox: Sandbox):
sandbox.pty.send_stdin(terminal.pid, b"exit\n")
result = terminal.wait()
assert result.exit_code == 0


def test_handle_send_stdin(sandbox: Sandbox):
output = []

terminal = sandbox.pty.create(PtySize(cols=80, rows=24), envs={"ABC": "123"})

# Send input directly through the handle instead of the PID-keyed module method.
terminal.send_stdin(b"echo $ABC\n")
terminal.send_stdin("exit\n")

result = terminal.wait(on_pty=lambda x: output.append(x.decode("utf-8")))
assert result.exit_code == 0
assert "123" in "".join(output)
Loading