Skip to content

Commit a47b7ea

Browse files
authored
fix: #3074 restore SIGINT defaults for UnixLocal PTY children (#3075)
1 parent 42c3015 commit a47b7ea

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

src/agents/sandbox/sandboxes/unix_local.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ async def pty_exec_start(
283283
def _preexec() -> None:
284284
os.setsid()
285285
fcntl.ioctl(secondary_fd, termios.TIOCSCTTY, 0)
286+
# PTY children should always treat Ctrl-C as an interrupt even if the parent
287+
# process temporarily ignores SIGINT under the test runner.
288+
signal.signal(signal.SIGINT, signal.SIG_DFL)
286289

287290
try:
288291
process = await asyncio.create_subprocess_exec(

tests/sandbox/test_unix_local.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import signal
34
from pathlib import Path
45

56
import pytest
@@ -106,6 +107,39 @@ async def test_pty_ctrl_c_interrupts_long_running_process(self, tmp_path: Path)
106107
with pytest.raises(PtySessionNotFoundError):
107108
await session.pty_write_stdin(session_id=started.process_id, chars="")
108109

110+
@pytest.mark.asyncio
111+
async def test_pty_ctrl_c_interrupts_even_if_parent_ignores_sigint(
112+
self, tmp_path: Path
113+
) -> None:
114+
client = UnixLocalSandboxClient()
115+
manifest = Manifest(root=str(tmp_path / "workspace"))
116+
previous_handler = signal.getsignal(signal.SIGINT)
117+
118+
signal.signal(signal.SIGINT, signal.SIG_IGN)
119+
try:
120+
async with await client.create(
121+
manifest=manifest, snapshot=None, options=None
122+
) as session:
123+
started = await session.pty_exec_start(
124+
"sleep",
125+
"30",
126+
shell=False,
127+
tty=True,
128+
yield_time_s=0.05,
129+
)
130+
assert started.process_id is not None
131+
132+
interrupted = await session.pty_write_stdin(
133+
session_id=started.process_id,
134+
chars="\x03",
135+
yield_time_s=5.5,
136+
)
137+
138+
assert interrupted.process_id is None
139+
assert interrupted.exit_code == -2
140+
finally:
141+
signal.signal(signal.SIGINT, previous_handler)
142+
109143
@pytest.mark.asyncio
110144
async def test_non_tty_pty_session_rejects_stdin_and_can_still_be_polled(
111145
self, tmp_path: Path

0 commit comments

Comments
 (0)