|
24 | 24 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer |
25 | 25 | from pydantic import BaseModel, Field |
26 | 26 |
|
27 | | -from open_terminal.env import API_KEY, BINARY_FILE_MIME_PREFIXES, CORS_ALLOWED_ORIGINS, ENABLE_NOTEBOOKS, ENABLE_SYSTEM_PROMPT, ENABLE_TERMINAL, EXECUTE_DESCRIPTION, EXECUTE_TIMEOUT, LOG_DIR, MAX_TERMINAL_SESSIONS, MULTI_USER, OPEN_TERMINAL_INFO, PROCESS_LOG_RETENTION, SYSTEM_PROMPT, TERMINAL_TERM |
| 27 | +from open_terminal.env import API_KEY, BINARY_FILE_MIME_PREFIXES, CORS_ALLOWED_ORIGINS, ENABLE_NOTEBOOKS, ENABLE_SYSTEM_PROMPT, ENABLE_TERMINAL, EXECUTE_DESCRIPTION, EXECUTE_TIMEOUT, LOG_DIR, MAX_TERMINAL_SESSIONS, MULTI_USER, OPEN_TERMINAL_INFO, PROCESS_LOG_RETENTION, SESSION_CWD_TTL, SYSTEM_PROMPT, TERMINAL_TERM |
28 | 28 | from open_terminal.utils.runner import PipeRunner, ProcessRunner, create_runner |
29 | 29 | from open_terminal.utils.fs import UserFS |
30 | 30 |
|
@@ -269,6 +269,40 @@ class BackgroundProcess: |
269 | 269 | _EXPIRY_SECONDS = 300 # auto-clean finished processes after 5 min |
270 | 270 |
|
271 | 271 |
|
| 272 | +# --------------------------------------------------------------------------- |
| 273 | +# Per-session working directory tracking |
| 274 | +# --------------------------------------------------------------------------- |
| 275 | +# Maps session_id → (absolute_cwd_path, last_accessed_timestamp). |
| 276 | +# Replaces the old os.chdir() approach which was process-global and unsafe |
| 277 | +# with concurrent sessions. |
| 278 | +_session_cwds: dict[str, tuple[str, float]] = {} |
| 279 | + |
| 280 | + |
| 281 | + |
| 282 | +def _expire_session_cwds(): |
| 283 | + """Remove session cwd entries that haven't been accessed within the TTL.""" |
| 284 | + now = time.time() |
| 285 | + expired = [sid for sid, (_, ts) in _session_cwds.items() if now - ts > SESSION_CWD_TTL] |
| 286 | + for sid in expired: |
| 287 | + del _session_cwds[sid] |
| 288 | + |
| 289 | + |
| 290 | +def _get_session_cwd(session_id: str | None, fs: "UserFS") -> str: |
| 291 | + """Return the tracked cwd for *session_id*, or ``fs.home`` as default.""" |
| 292 | + _expire_session_cwds() |
| 293 | + if session_id and session_id in _session_cwds: |
| 294 | + cwd, _ = _session_cwds[session_id] |
| 295 | + _session_cwds[session_id] = (cwd, time.time()) # refresh TTL |
| 296 | + return cwd |
| 297 | + return fs.home |
| 298 | + |
| 299 | + |
| 300 | +def _set_session_cwd(session_id: str | None, path: str): |
| 301 | + """Store a session's cwd. No-op if *session_id* is ``None``.""" |
| 302 | + if session_id: |
| 303 | + _session_cwds[session_id] = (path, time.time()) |
| 304 | + |
| 305 | + |
272 | 306 | from open_terminal.utils.log import log_process, read_log |
273 | 307 |
|
274 | 308 |
|
@@ -378,26 +412,29 @@ async def get_info(): |
378 | 412 | include_in_schema=False, |
379 | 413 | dependencies=[Depends(verify_api_key)], |
380 | 414 | ) |
381 | | -async def get_cwd(fs: UserFS = Depends(get_filesystem)): |
382 | | - return {"cwd": fs.home} |
| 415 | +async def get_cwd( |
| 416 | + http_request: Request, |
| 417 | + fs: UserFS = Depends(get_filesystem), |
| 418 | +): |
| 419 | + session_id = http_request.headers.get("x-session-id") |
| 420 | + return {"cwd": _get_session_cwd(session_id, fs)} |
383 | 421 |
|
384 | 422 |
|
385 | 423 | @app.post( |
386 | 424 | "/files/cwd", |
387 | 425 | include_in_schema=False, |
388 | 426 | dependencies=[Depends(verify_api_key)], |
389 | 427 | ) |
390 | | -async def set_cwd(request: MkdirRequest, fs: UserFS = Depends(get_filesystem)): |
| 428 | +async def set_cwd( |
| 429 | + http_request: Request, |
| 430 | + request: MkdirRequest, |
| 431 | + fs: UserFS = Depends(get_filesystem), |
| 432 | +): |
| 433 | + session_id = http_request.headers.get("x-session-id") |
391 | 434 | target = fs.resolve_path(request.path) |
392 | | - if fs.username: |
393 | | - # In multi-user mode, cwd is per-user; don't touch the global server cwd. |
394 | | - return {"cwd": target} |
395 | | - if not await fs.isdir(target): |
| 435 | + if not fs.username and not await fs.isdir(target): |
396 | 436 | raise HTTPException(status_code=404, detail="Directory not found") |
397 | | - try: |
398 | | - os.chdir(target) |
399 | | - except OSError as e: |
400 | | - raise HTTPException(status_code=400, detail=str(e)) |
| 437 | + _set_session_cwd(session_id, target) |
401 | 438 | return {"cwd": target} |
402 | 439 |
|
403 | 440 |
|
@@ -1055,7 +1092,8 @@ async def execute( |
1055 | 1092 | ), |
1056 | 1093 | ): |
1057 | 1094 | fs = get_filesystem(http_request) |
1058 | | - cwd = fs.resolve_path(request.cwd) if request.cwd else (fs.home if fs.username else None) |
| 1095 | + session_id = http_request.headers.get("x-session-id") |
| 1096 | + cwd = fs.resolve_path(request.cwd) if request.cwd else _get_session_cwd(session_id, fs) |
1059 | 1097 |
|
1060 | 1098 | subprocess_env = {**os.environ, **request.env} if request.env else None |
1061 | 1099 | runner = await create_runner( |
@@ -1437,16 +1475,21 @@ async def create_terminal(request: Request): |
1437 | 1475 | fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0)) |
1438 | 1476 |
|
1439 | 1477 | fs = get_filesystem(request) |
| 1478 | + |
| 1479 | + # Use per-session cwd if available, else fall back to home |
| 1480 | + session_id = request.headers.get("x-session-id") |
| 1481 | + session_cwd = _get_session_cwd(session_id, fs) if session_id else None |
| 1482 | + |
1440 | 1483 | if fs.username: |
1441 | 1484 | shell_cmd = [ |
1442 | 1485 | "script", "-qc", |
1443 | 1486 | f"sudo -i -u {fs.username}", |
1444 | 1487 | "/dev/null", |
1445 | 1488 | ] |
1446 | | - cwd = fs.home |
| 1489 | + cwd = session_cwd or fs.home |
1447 | 1490 | else: |
1448 | 1491 | shell_cmd = [os.environ.get("SHELL", "/bin/sh")] |
1449 | | - cwd = os.getcwd() |
| 1492 | + cwd = session_cwd or os.getcwd() |
1450 | 1493 |
|
1451 | 1494 | spawn_env = os.environ.copy() |
1452 | 1495 | spawn_env.setdefault("TERM", TERMINAL_TERM) |
|
0 commit comments