Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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-stdin.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 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.
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 @@
undefined,
undefined,
opts.onData,
undefined,
(data, stdinOpts) => this.sendStdin(pid, data, stdinOpts),
undefined,
this.checkHealth
)
Expand Down Expand Up @@ -210,7 +210,7 @@
undefined,
undefined,
opts?.onData,
undefined,
(data, stdinOpts) => this.sendStdin(pid, data, stdinOpts),
undefined,
this.checkHealth
)
Expand All @@ -227,9 +227,9 @@
* @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 @@
input: {
input: {
case: 'pty',
value: data,
value:
typeof data === 'string'
? new TextEncoder().encode(data)
: data,
},
},
process: {
Expand All @@ -260,6 +263,23 @@
}
}

/**
* 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)
}

Check warning on line 282 in packages/js-sdk/src/sandbox/commands/pty.ts

View check run for this annotation

Claude / Claude Code Review

Migrate CLI from deprecated pty.sendInput

The CLI at packages/cli/src/terminal.ts:29 still calls the freshly-deprecated `sandbox.pty.sendInput(...)`. Since this PR adds the `@deprecated` marker, it introduces a TypeScript deprecation warning against its own internal consumer — migrating the single call site to `sandbox.pty.sendStdin` keeps the monorepo clean of warnings this PR itself produces. Nit — functionally identical (the alias just delegates).
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