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

Support sending input to a PTY directly through its handle with `handle.sendInput()` / `handle.send_input()`. Previously input could only be sent via the PID-keyed `sandbox.pty.sendInput()` / `sandbox.pty.send_stdin()`, and calling `handle.send_stdin()` on a PTY raised "Sending stdin is not supported for this command handle." It now raises an error pointing to `send_input()`, since a PTY has no stdin. PTY input methods also accept `str` input, encoded as UTF-8.

In the Python SDK, `sandbox.pty.send_input()` is added to match the JS SDK; `sandbox.pty.send_stdin()` remains as a deprecated alias.
34 changes: 32 additions & 2 deletions packages/js-sdk/src/sandbox/commands/commandHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ export class CommandHandle<Output extends string | undefined = string>
opts?: CommandRequestOpts
) => Promise<void>,
private readonly checkHealth?: SandboxHealthCheck,
retainOutput = true
retainOutput = true,
private readonly handleSendInput?: (
data: string | Uint8Array,
opts?: CommandRequestOpts
) => Promise<void>
) {
if (retainOutput) {
this._stdout = ''
Expand Down Expand Up @@ -248,13 +252,39 @@ export class CommandHandle<Output extends string | undefined = string>
): Promise<void> {
if (!this.handleSendStdin) {
throw new SandboxError(
'Sending stdin is not supported for this command handle.'
this.handleSendInput
? 'PTY handles have no stdin, use sendInput() to send input to the PTY instead.'
: 'Sending stdin is not supported for this command handle.'
)
}

await this.handleSendStdin(data, opts)
}

/**
* Send input to the PTY.
*
* Only available on handles returned by `sandbox.pty.create()` and `sandbox.pty.connect()`.
* Strings are encoded as UTF-8.
*
* @param data input data to send to the PTY.
* @param opts connection options.
*/
async sendInput(
data: string | Uint8Array,
opts?: CommandRequestOpts
): Promise<void> {
if (!this.handleSendInput) {
throw new SandboxError(
this.handleSendStdin
? 'Sending input is only supported for PTY handles, use sendStdin() to send data to the command stdin instead.'
: 'Sending input is not supported for this command handle.'
)
}

await this.handleSendInput(data, opts)
}

/**
* Close the command stdin.
*
Expand Down
15 changes: 11 additions & 4 deletions packages/js-sdk/src/sandbox/commands/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ export class Pty {
opts.onData,
undefined,
undefined,
this.checkHealth
this.checkHealth,
undefined,
(data, inputOpts) => this.sendInput(pid, data, inputOpts)
)
} catch (err) {
cleanup()
Expand Down Expand Up @@ -210,7 +212,9 @@ export class Pty {
opts?.onData,
undefined,
undefined,
this.checkHealth
this.checkHealth,
undefined,
(data, inputOpts) => this.sendInput(pid, data, inputOpts)
)
} catch (err) {
cleanup()
Expand All @@ -227,7 +231,7 @@ export class Pty {
*/
async sendInput(
pid: number,
data: Uint8Array,
data: string | Uint8Array,
opts?: Pick<ConnectionOpts, 'requestTimeoutMs' | 'signal'>
): Promise<void> {
try {
Expand All @@ -236,7 +240,10 @@ export class Pty {
input: {
input: {
case: 'pty',
value: data,
value:
typeof data === 'string'
? new TextEncoder().encode(data)
: data,
},
},
process: {
Expand Down
56 changes: 56 additions & 0 deletions packages/js-sdk/tests/sandbox/commands/commandHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,59 @@ describe('CommandHandle', () => {
expect(err.stderr).toBeUndefined()
})
})

describe('CommandHandle input routing', () => {
async function* noEvents() {}

function ptyHandle(
sendInput: (data: string | Uint8Array) => Promise<void>
): CommandHandle {
return new CommandHandle(
1,
() => {},
async () => true,
noEvents(),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
sendInput
)
}

it('sends PTY input through sendInput', async () => {
const sent: (string | Uint8Array)[] = []
const handle = ptyHandle(async (data) => {
sent.push(data)
})

await handle.sendInput('ls\n')
await handle.sendInput(new Uint8Array([1, 2]))

expect(sent).toEqual(['ls\n', new Uint8Array([1, 2])])
})

it('points PTY handles from sendStdin to sendInput', async () => {
const handle = ptyHandle(async () => {})

await expect(handle.sendStdin('ls\n')).rejects.toThrow(/use sendInput\(\)/)
})

it('points command handles from sendInput to sendStdin', async () => {
const handle = new CommandHandle(
1,
() => {},
async () => true,
noEvents(),
undefined,
undefined,
undefined,
async () => {}
)

await expect(handle.sendInput('ls\n')).rejects.toThrow(/use sendStdin\(\)/)
})
})
34 changes: 34 additions & 0 deletions packages/js-sdk/tests/sandbox/pty/sendInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,37 @@ sandboxTest('send input', async ({ sandbox }) => {
await terminal.wait()
expect(terminal.exitCode).toBe(0)
})

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

await sandbox.pty.sendInput(terminal.pid, 'exit\n')

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

sandboxTest('send input 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.sendInput(new TextEncoder().encode('echo $ABC\n'))
await terminal.sendInput('exit\n')

await terminal.wait()
expect(terminal.exitCode).toBe(0)
expect(output).toContain('123')
})
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,15 @@ def __init__(
] = None,
check_health: Optional[Callable[[], Awaitable[Optional[bool]]]] = None,
retain_output: bool = True,
handle_send_input: Optional[
Callable[[Union[str, bytes], Optional[float]], Coroutine[Any, Any, None]]
] = None,
):
self._pid = pid
self._handle_kill = handle_kill
self._handle_send_stdin = handle_send_stdin
self._handle_close_stdin = handle_close_stdin
self._handle_send_input = handle_send_input
self._check_health = check_health
self._events = events

Expand Down Expand Up @@ -304,11 +308,38 @@ async def send_stdin(
:param request_timeout: Timeout for the request in **seconds**
"""
if self._handle_send_stdin is None:
if self._handle_send_input is not None:
raise SandboxException(
"PTY handles have no stdin, use send_input() to send input to the PTY instead."
)
raise SandboxException(
"Sending stdin is not supported for this command handle."
)
await self._handle_send_stdin(data, request_timeout)

async def send_input(
self,
data: Union[str, bytes],
request_timeout: Optional[float] = None,
) -> None:
"""
Send input to the PTY.

Only available on handles returned by `sandbox.pty.create()` and `sandbox.pty.connect()`.

:param data: Input data to send, `str` is encoded as UTF-8
:param request_timeout: Timeout for the request in **seconds**
"""
if self._handle_send_input is None:
if self._handle_send_stdin is not None:
raise SandboxException(
"Sending input is only supported for PTY handles, use send_stdin() to send data to the command stdin instead."
)
raise SandboxException(
"Sending input is not supported for this command handle."
)
await self._handle_send_input(data, request_timeout)

async def close_stdin(self, request_timeout: Optional[float] = None) -> None:
"""
Close the command stdin.
Expand Down
33 changes: 29 additions & 4 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 httpx

Expand Down Expand Up @@ -85,22 +85,41 @@ async def kill(
async def send_stdin(
self,
pid: int,
data: bytes,
data: Union[str, bytes],
request_timeout: Optional[float] = None,
) -> None:
"""
Send input to a PTY.

:deprecated: Use `send_input()` instead. PTYs have no stdin; this sends input to the PTY.

:param pid: Process ID of the PTY
:param data: Input data to send, `str` is encoded as UTF-8
:param request_timeout: Timeout for the request in **seconds**
"""
return await self.send_input(pid, data, request_timeout)

async def send_input(
self,
pid: int,
data: Union[str, bytes],
request_timeout: Optional[float] = None,
) -> None:
"""
Send input to a PTY.

:param pid: Process ID of the PTY
:param data: Input data to send
:param data: Input data to send, `str` is encoded as UTF-8
:param request_timeout: Timeout for the request in **seconds**
"""
try:
await self._rpc.send_input(
process_pb.SendInputRequest(
process=process_pb.ProcessSelector(selector=Oneof("pid", pid)),
input=process_pb.ProcessInput(
input=Oneof("pty", data),
input=Oneof(
"pty", data.encode() if isinstance(data, str) else data
),
),
),
timeout_ms=timeout_to_ms(
Expand Down Expand Up @@ -195,6 +214,9 @@ async def create(
handle_kill=lambda: self.kill(pid),
events=events,
on_pty=on_data,
handle_send_input=lambda data, request_timeout=None: self.send_input(
pid, data, request_timeout
),
check_health=self._check_health,
)
except Exception as e:
Expand Down Expand Up @@ -244,6 +266,9 @@ async def connect(
handle_kill=lambda: self.kill(pid),
events=events,
on_pty=on_data,
handle_send_input=lambda data, request_timeout=None: self.send_input(
pid, data, request_timeout
),
check_health=self._check_health,
)
except Exception as e:
Expand Down
31 changes: 31 additions & 0 deletions packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ def __init__(
handle_close_stdin: Optional[Callable[[Optional[float]], None]] = None,
check_health: Optional[Callable[[], Optional[bool]]] = None,
retain_output: bool = True,
handle_send_input: Optional[
Callable[[Union[str, bytes], Optional[float]], None]
] = None,
):
self._pid = pid
self._handle_kill = handle_kill
self._handle_send_stdin = handle_send_stdin
self._handle_close_stdin = handle_close_stdin
self._handle_send_input = handle_send_input
self._check_health = check_health
self._events = events

Expand Down Expand Up @@ -240,11 +244,38 @@ def send_stdin(
:param request_timeout: Timeout for the request in **seconds**
"""
if self._handle_send_stdin is None:
if self._handle_send_input is not None:
raise SandboxException(
"PTY handles have no stdin, use send_input() to send input to the PTY instead."
)
raise SandboxException(
"Sending stdin is not supported for this command handle."
)
self._handle_send_stdin(data, request_timeout)

def send_input(
self,
data: Union[str, bytes],
request_timeout: Optional[float] = None,
) -> None:
"""
Send input to the PTY.

Only available on handles returned by `sandbox.pty.create()` and `sandbox.pty.connect()`.

:param data: Input data to send, `str` is encoded as UTF-8
:param request_timeout: Timeout for the request in **seconds**
"""
if self._handle_send_input is None:
if self._handle_send_stdin is not None:
raise SandboxException(
"Sending input is only supported for PTY handles, use send_stdin() to send data to the command stdin instead."
)
raise SandboxException(
"Sending input is not supported for this command handle."
)
self._handle_send_input(data, request_timeout)

def close_stdin(self, request_timeout: Optional[float] = None) -> None:
"""
Close the command stdin.
Expand Down
Loading
Loading