fix: send SIGWINCH to docker exec after PTY resize
When resizing the PTY, docker exec needs to be notified so it can re-read the terminal size and propagate it to the container's PTY. Without this, the container shell stays at 80x24 regardless of what the frontend sends.
This commit is contained in:
@@ -148,10 +148,10 @@ async def _write_loop(session, websocket) -> None:
|
|||||||
msg_type = ctrl.get("type")
|
msg_type = ctrl.get("type")
|
||||||
|
|
||||||
if msg_type == "resize":
|
if msg_type == "resize":
|
||||||
await session.resize(
|
cols = ctrl.get("cols", 80)
|
||||||
ctrl.get("cols", 80),
|
rows = ctrl.get("rows", 24)
|
||||||
ctrl.get("rows", 24),
|
logger.info(f"Received resize message for instance {instance_id}: {cols}x{rows}")
|
||||||
)
|
await session.resize(cols, rows)
|
||||||
elif msg_type == "reset":
|
elif msg_type == "reset":
|
||||||
# Reset terminal session
|
# Reset terminal session
|
||||||
logger.info("Resetting terminal session for instance %s", session.instance_id)
|
logger.info("Resetting terminal session for instance %s", session.instance_id)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Terminal session management for tool instances."""
|
"""Terminal session management for tool instances."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pty
|
import pty
|
||||||
import select
|
import select
|
||||||
|
import signal
|
||||||
import struct
|
import struct
|
||||||
import fcntl
|
import fcntl
|
||||||
import time
|
import time
|
||||||
@@ -11,6 +13,8 @@ import uuid
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TerminalSession:
|
class TerminalSession:
|
||||||
"""Manages a single terminal session connected to a docker container.
|
"""Manages a single terminal session connected to a docker container.
|
||||||
@@ -55,6 +59,7 @@ class TerminalSession:
|
|||||||
|
|
||||||
# Set the terminal size initially
|
# Set the terminal size initially
|
||||||
self._set_terminal_size(self._cols, self._rows)
|
self._set_terminal_size(self._cols, self._rows)
|
||||||
|
logger.info(f"Starting terminal session {self.session_id} for container {self.container_id} with initial size {self._cols}x{self._rows}")
|
||||||
|
|
||||||
# Start docker exec with the slave fd as stdin/stdout/stderr
|
# Start docker exec with the slave fd as stdin/stdout/stderr
|
||||||
# Using -it because the slave fd IS a TTY
|
# Using -it because the slave fd IS a TTY
|
||||||
@@ -79,16 +84,27 @@ class TerminalSession:
|
|||||||
self.last_activity = time.time()
|
self.last_activity = time.time()
|
||||||
|
|
||||||
def _set_terminal_size(self, cols: int, rows: int) -> None:
|
def _set_terminal_size(self, cols: int, rows: int) -> None:
|
||||||
"""Set the terminal size using TIOCSWINSZ."""
|
"""Set the terminal size using TIOCSWINSZ and signal docker exec."""
|
||||||
if self._master_fd is None:
|
if self._master_fd is None:
|
||||||
|
logger.warning("Cannot resize: master_fd is None (session not started)")
|
||||||
return
|
return
|
||||||
# TIOCSWINSZ = 0x5414 on Linux
|
# TIOCSWINSZ = 0x5414 on Linux
|
||||||
TIOCSWINSZ = 0x5414
|
TIOCSWINSZ = 0x5414
|
||||||
size = struct.pack('HHHH', rows, cols, 0, 0)
|
size = struct.pack('HHHH', rows, cols, 0, 0)
|
||||||
try:
|
try:
|
||||||
fcntl.ioctl(self._master_fd, TIOCSWINSZ, size)
|
fcntl.ioctl(self._master_fd, TIOCSWINSZ, size)
|
||||||
except (OSError, IOError):
|
logger.info(f"Resized PTY to {cols}x{rows} (fd={self._master_fd})")
|
||||||
pass
|
|
||||||
|
# Send SIGWINCH to docker exec process so it re-reads terminal
|
||||||
|
# size and propagates it to the container's PTY
|
||||||
|
if self.process and self.process.pid:
|
||||||
|
try:
|
||||||
|
os.kill(self.process.pid, signal.SIGWINCH)
|
||||||
|
logger.info(f"Sent SIGWINCH to docker exec pid={self.process.pid}")
|
||||||
|
except (OSError, ProcessLookupError) as e:
|
||||||
|
logger.warning(f"Failed to send SIGWINCH: {e}")
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
logger.error(f"Failed to resize PTY: {e}")
|
||||||
|
|
||||||
async def read_output(self) -> bytes:
|
async def read_output(self) -> bytes:
|
||||||
"""Read output from the PTY master and store in buffer."""
|
"""Read output from the PTY master and store in buffer."""
|
||||||
@@ -134,9 +150,11 @@ class TerminalSession:
|
|||||||
async def resize(self, cols: int, rows: int) -> None:
|
async def resize(self, cols: int, rows: int) -> None:
|
||||||
"""Resize the terminal."""
|
"""Resize the terminal."""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
|
logger.warning("Cannot resize: session is closed")
|
||||||
return
|
return
|
||||||
self._cols = cols
|
self._cols = cols
|
||||||
self._rows = rows
|
self._rows = rows
|
||||||
|
logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}")
|
||||||
self._set_terminal_size(cols, rows)
|
self._set_terminal_size(cols, rows)
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def reset(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user