mirror of
https://github.com/open-webui/open-terminal.git
synced 2026-07-09 16:09:14 +00:00
fix: PTY device exhaustion and session limit (0.7.2)
- Close leaked PTY file descriptors when subprocess creation fails - Return 503 with descriptive message when PTY devices are exhausted - Add MAX_TERMINAL_SESSIONS (default 16, env OPEN_TERMINAL_MAX_SESSIONS) - Prune dead sessions before checking the session limit - Bump version to 0.7.2
This commit is contained in:
parent
80c3aee5fb
commit
840f538d79
5 changed files with 76 additions and 28 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.7.2] - 2026-03-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐳 **PTY device exhaustion** — fixed `OSError: out of pty devices` by closing leaked file descriptors when subprocess creation fails after `pty.openpty()`. Both `PtyRunner` (command execution) and `create_terminal` (interactive sessions) now properly clean up on error paths.
|
||||
- 🛡️ **Graceful PTY error handling** — `create_terminal` now returns a clear `503` with a descriptive message when the system runs out of PTY devices, instead of an unhandled server error.
|
||||
|
||||
### Added
|
||||
|
||||
- 🔒 **Terminal session limit** — new `OPEN_TERMINAL_MAX_SESSIONS` environment variable (default `16`) caps the number of concurrent interactive terminal sessions. Dead sessions are automatically pruned before the limit is checked. Returns `429` when the limit is reached.
|
||||
|
||||
## [0.7.1] - 2026-03-02
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -58,3 +58,10 @@ BINARY_FILE_MIME_PREFIXES = [
|
|||
).split(",")
|
||||
if p.strip()
|
||||
]
|
||||
|
||||
MAX_TERMINAL_SESSIONS = int(
|
||||
os.environ.get(
|
||||
"OPEN_TERMINAL_MAX_SESSIONS",
|
||||
config.get("max_terminal_sessions", "16"),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|||
from pydantic import BaseModel, Field
|
||||
from pypdf import PdfReader
|
||||
|
||||
from open_terminal.env import API_KEY, BINARY_FILE_MIME_PREFIXES, CORS_ALLOWED_ORIGINS, LOG_DIR
|
||||
from open_terminal.env import API_KEY, BINARY_FILE_MIME_PREFIXES, CORS_ALLOWED_ORIGINS, LOG_DIR, MAX_TERMINAL_SESSIONS
|
||||
from open_terminal.runner import PipeRunner, ProcessRunner, create_runner
|
||||
|
||||
try:
|
||||
|
|
@ -69,7 +69,7 @@ async def verify_api_key(
|
|||
app = FastAPI(
|
||||
title="Open Terminal",
|
||||
description="A remote terminal API.",
|
||||
version="0.7.1",
|
||||
version="0.7.2",
|
||||
)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
@ -1140,6 +1140,7 @@ def _cleanup_session(session_id: str):
|
|||
process.kill()
|
||||
|
||||
|
||||
|
||||
@app.post("/api/terminals", dependencies=[Depends(verify_api_key)], include_in_schema=False)
|
||||
async def create_terminal(request: Request):
|
||||
"""Create a new terminal session and return its ID."""
|
||||
|
|
@ -1148,20 +1149,44 @@ async def create_terminal(request: Request):
|
|||
{"error": "PTY not available on this platform"}, status_code=503
|
||||
)
|
||||
|
||||
session_id = str(_uuid.uuid4())[:8]
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
||||
# Prune dead sessions before checking limit
|
||||
dead = [sid for sid, s in _terminal_sessions.items() if s["process"].poll() is not None]
|
||||
for sid in dead:
|
||||
_cleanup_session(sid)
|
||||
|
||||
shell = os.environ.get("SHELL", "/bin/sh")
|
||||
process = subprocess.Popen(
|
||||
[shell],
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
cwd=os.getcwd(),
|
||||
env=os.environ.copy(),
|
||||
start_new_session=True,
|
||||
)
|
||||
if len(_terminal_sessions) >= MAX_TERMINAL_SESSIONS:
|
||||
return JSONResponse(
|
||||
{"error": f"Maximum number of terminal sessions ({MAX_TERMINAL_SESSIONS}) reached"},
|
||||
status_code=429,
|
||||
)
|
||||
|
||||
session_id = str(_uuid.uuid4())[:8]
|
||||
|
||||
try:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
except OSError:
|
||||
return JSONResponse(
|
||||
{"error": "Out of PTY devices — too many active terminals or processes"},
|
||||
status_code=503,
|
||||
)
|
||||
|
||||
try:
|
||||
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
||||
|
||||
shell = os.environ.get("SHELL", "/bin/sh")
|
||||
process = subprocess.Popen(
|
||||
[shell],
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
cwd=os.getcwd(),
|
||||
env=os.environ.copy(),
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception:
|
||||
os.close(slave_fd)
|
||||
os.close(master_fd)
|
||||
raise
|
||||
os.close(slave_fd)
|
||||
|
||||
# Set non-blocking
|
||||
|
|
|
|||
|
|
@ -51,18 +51,23 @@ class PtyRunner(ProcessRunner):
|
|||
|
||||
def __init__(self, command: str, cwd: str | None, env: dict | None):
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
# Set a reasonable default window size (80x24).
|
||||
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
||||
self._process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
)
|
||||
try:
|
||||
# Set a reasonable default window size (80x24).
|
||||
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
||||
self._process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception:
|
||||
os.close(slave_fd)
|
||||
os.close(master_fd)
|
||||
raise
|
||||
os.close(slave_fd)
|
||||
self._master_fd = master_fd
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "open-terminal"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
description = "A remote terminal API."
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue