Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException, status, UploadFile, File, Path as FastAPIPath | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse, Response, HTMLResponse | |
| import logging | |
| from contextlib import asynccontextmanager | |
| from typing import List | |
| from sandbox.executor import SandboxExecutor | |
| from sandbox.session_manager import SessionManager | |
| from sandbox.file_manager import FileManager | |
| from sandbox.container_builder import ContainerBuilder | |
| from sandbox.models import ( | |
| ExecutionRequest, ExecutionResponse, SandboxConfig, Language, | |
| CreateSessionRequest, SessionResponse, FileInfo, FileUploadResponse, | |
| ExecuteInSessionRequest, ExecuteFileRequest | |
| ) | |
| from sandbox.language_runners import LanguageRunner | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Global instances | |
| executor: SandboxExecutor = None | |
| session_manager: SessionManager = None | |
| file_manager: FileManager = None | |
| async def lifespan(app: FastAPI): | |
| """Initialize and cleanup resources""" | |
| global executor, session_manager, file_manager | |
| docker_available = False | |
| try: | |
| # Try to initialize Docker services | |
| logger.info("Checking Docker availability...") | |
| import docker | |
| try: | |
| docker_client = docker.from_env() | |
| docker_client.ping() | |
| docker_available = True | |
| logger.info("Docker is available, initializing full services...") | |
| except Exception as docker_error: | |
| logger.warning(f"Docker is not available: {docker_error}") | |
| logger.warning("Running in limited mode without Docker support") | |
| logger.warning("Session management and file operations will be disabled") | |
| docker_available = False | |
| if docker_available: | |
| # Build/verify devenv image | |
| logger.info("Checking development environment image...") | |
| try: | |
| builder = ContainerBuilder() | |
| if not builder.ensure_devenv_image(): | |
| logger.warning("Dev environment image not available, sessions will fail") | |
| except Exception as e: | |
| logger.warning(f"Could not check/build devenv image: {e}") | |
| # Initialize services | |
| logger.info("Initializing sandbox services...") | |
| try: | |
| executor = SandboxExecutor(SandboxConfig()) | |
| session_manager = SessionManager(SandboxConfig()) | |
| file_manager = FileManager() | |
| logger.info("All services initialized successfully") | |
| except Exception as e: | |
| logger.error(f"Failed to initialize some services: {e}") | |
| logger.warning("Some features may be unavailable") | |
| else: | |
| logger.info("Skipping Docker-dependent service initialization") | |
| yield | |
| except Exception as e: | |
| logger.error(f"Lifespan error: {e}", exc_info=True) | |
| # Don't raise - allow app to start even with limited functionality | |
| yield | |
| finally: | |
| # Cleanup on shutdown | |
| if session_manager: | |
| try: | |
| logger.info("Shutting down session manager...") | |
| session_manager.shutdown() | |
| except: | |
| pass | |
| app = FastAPI( | |
| title="isolated-sandbox", | |
| description="Execute code in isolated containers with persistent VM-like sessions and file system operations", | |
| version="2.0.0", | |
| lifespan=lifespan | |
| ) | |
| # CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def root(): | |
| """Root endpoint with API information""" | |
| docker_available = executor is not None and hasattr(executor, 'client') and executor.client is not None | |
| if docker_available: | |
| try: | |
| executor.client.ping() | |
| docker_available = True | |
| except: | |
| docker_available = False | |
| return { | |
| "name": "isolated-sandbox", | |
| "version": "2.0.0", | |
| "status": "running", | |
| "docker_available": docker_available, | |
| "features": { | |
| "stateless_execution": "/execute" if docker_available else "unavailable (Docker required)", | |
| "persistent_sessions": "/sessions" if docker_available else "unavailable (Docker required)", | |
| "file_operations": docker_available, | |
| "multi_language": docker_available | |
| }, | |
| "supported_languages": ["python", "javascript", "bash"] if docker_available else [], | |
| "endpoints": { | |
| "execute": "/execute (stateless)" if docker_available else "unavailable", | |
| "sessions": "/sessions (create/list)" if docker_available else "unavailable", | |
| "session_detail": "/sessions/{session_id}" if docker_available else "unavailable", | |
| "files": "/sessions/{session_id}/files" if docker_available else "unavailable", | |
| "execute_in_session": "/sessions/{session_id}/execute" if docker_available else "unavailable", | |
| "languages": "/languages", | |
| "health": "/health", | |
| "docs": "/docs", | |
| "ui-rules": "/ui-rules" | |
| }, | |
| "notice": "Docker is not available. Full functionality requires Docker. See /docs for API documentation." if not docker_available else None | |
| } | |
| def health_check(): | |
| """Health check endpoint""" | |
| try: | |
| docker_status = "unavailable" | |
| session_count = 0 | |
| if executor and hasattr(executor, 'client') and executor.client: | |
| try: | |
| executor.client.ping() | |
| docker_status = "connected" | |
| # Check session manager | |
| session_count = len(session_manager.sessions) if session_manager else 0 | |
| except Exception: | |
| docker_status = "disconnected" | |
| # App is healthy even without Docker | |
| return { | |
| "status": "healthy", | |
| "docker": docker_status, | |
| "active_sessions": session_count, | |
| "features_available": docker_status == "connected" | |
| } | |
| except Exception as e: | |
| logger.error(f"Health check failed: {e}") | |
| # Still return healthy status, just with error info | |
| return { | |
| "status": "healthy", | |
| "docker": "unknown", | |
| "active_sessions": 0, | |
| "features_available": False, | |
| "warning": str(e) | |
| } | |
| def list_languages(): | |
| """List all supported programming languages""" | |
| return { | |
| "languages": LanguageRunner.get_all_languages() | |
| } | |
| def ui_rules_docs(): | |
| """UI/UX Design Rules Documentation""" | |
| html_content = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0"> | |
| <title>UI/UX Rules - isolated-sandbox</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg: #ffffff; | |
| --text: #1a1a1a; | |
| --accent: #0066cc; | |
| --border: #e0e0e0; | |
| --code-bg: #f5f5f5; | |
| --must: #c53030; | |
| --should: #d69e2e; | |
| --never: #742a2a; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg: #1a1a1a; | |
| --text: #e0e0e0; | |
| --accent: #4a9eff; | |
| --border: #333; | |
| --code-bg: #2a2a2a; | |
| --must: #ff6b6b; | |
| --should: #ffd93d; | |
| --never: #ff8787; | |
| } | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| line-height: 1.6; | |
| color: var(--text); | |
| background: var(--bg); | |
| padding: 2rem; | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 0.5rem; | |
| scroll-margin-top: 2rem; | |
| } | |
| h2 { | |
| font-size: 1.75rem; | |
| margin-top: 3rem; | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 2px solid var(--border); | |
| scroll-margin-top: 2rem; | |
| } | |
| h3 { | |
| font-size: 1.25rem; | |
| margin-top: 2rem; | |
| margin-bottom: 0.75rem; | |
| scroll-margin-top: 2rem; | |
| } | |
| p { | |
| margin-bottom: 1rem; | |
| } | |
| ul, ol { | |
| margin-left: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| li { | |
| margin-bottom: 0.5rem; | |
| } | |
| code { | |
| background: var(--code-bg); | |
| padding: 0.2em 0.4em; | |
| border-radius: 3px; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| font-size: 0.9em; | |
| } | |
| pre { | |
| background: var(--code-bg); | |
| padding: 1rem; | |
| border-radius: 6px; | |
| overflow-x: auto; | |
| margin: 1rem 0; | |
| } | |
| pre code { | |
| background: none; | |
| padding: 0; | |
| } | |
| .must { | |
| color: var(--must); | |
| font-weight: 600; | |
| } | |
| .should { | |
| color: var(--should); | |
| font-weight: 600; | |
| } | |
| .never { | |
| color: var(--never); | |
| font-weight: 600; | |
| } | |
| .rule-item { | |
| margin-bottom: 1rem; | |
| padding-left: 1rem; | |
| border-left: 3px solid var(--border); | |
| } | |
| .rule-item .must { | |
| border-left-color: var(--must); | |
| } | |
| .rule-item .should { | |
| border-left-color: var(--should); | |
| } | |
| .rule-item .never { | |
| border-left-color: var(--never); | |
| } | |
| a { | |
| color: var(--accent); | |
| text-decoration: none; | |
| } | |
| a:hover, a:focus { | |
| text-decoration: underline; | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| a:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| nav { | |
| margin-bottom: 2rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| nav a { | |
| margin-right: 1rem; | |
| } | |
| @media (max-width: 768px) { | |
| body { | |
| padding: 1rem; | |
| } | |
| h1 { | |
| font-size: 2rem; | |
| } | |
| h2 { | |
| font-size: 1.5rem; | |
| } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| * { | |
| animation-duration: 0.01ms !important; | |
| animation-iteration-count: 1 !important; | |
| transition-duration: 0.01ms !important; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <a href="/">Home</a> | |
| <a href="/docs">API Docs</a> | |
| <a href="/ui-rules">UI Rules</a> | |
| </nav> | |
| <main> | |
| <h1>UI/UX Design Rules</h1> | |
| <p>Concise rules for building accessible, fast, delightful UIs. Use <span class="must">MUST</span>/<span class="should">SHOULD</span>/<span class="never">NEVER</span> to guide decisions.</p> | |
| <h2>Interactions</h2> | |
| <h3>Keyboard</h3> | |
| <ul> | |
| <li class="rule-item"><span class="must">MUST:</span> Full keyboard support per <a href="https://www.w3.org/WAI/ARIA/apg/patterns/" target="_blank" rel="noopener">WAI-ARIA APG</a></li> | |
| <li class="rule-item"><span class="must">MUST:</span> Visible focus rings (<code>:focus-visible</code>; group with <code>:focus-within</code>)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Manage focus (trap, move, and return) per APG patterns</li> | |
| </ul> | |
| <h3>Targets & Input</h3> | |
| <ul> | |
| <li class="rule-item"><span class="must">MUST:</span> Hit target ≥24px (mobile ≥44px) If visual <24px, expand hit area</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Mobile <code><input></code> font-size ≥16px or set: | |
| <pre><code><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"></code></pre> | |
| </li> | |
| <li class="rule-item"><span class="never">NEVER:</span> Disable browser zoom</li> | |
| <li class="rule-item"><span class="must">MUST:</span> <code>touch-action: manipulation</code> to prevent double-tap zoom; set <code>-webkit-tap-highlight-color</code> to match design</li> | |
| </ul> | |
| <h3>Inputs & Forms (Behavior)</h3> | |
| <ul> | |
| <li class="rule-item"><span class="must">MUST:</span> Hydration-safe inputs (no lost focus/value)</li> | |
| <li class="rule-item"><span class="never">NEVER:</span> Block paste in <code><input>/<textarea></code></li> | |
| <li class="rule-item"><span class="must">MUST:</span> Loading buttons show spinner and keep original label</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Enter submits focused text input In <code><textarea></code>, ⌘/Ctrl+Enter submits; Enter adds newline</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Keep submit enabled until request starts; then disable, show spinner, use idempotency key</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Don't block typing; accept free text and validate after</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Allow submitting incomplete forms to surface validation</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Errors inline next to fields; on submit, focus first error</li> | |
| <li class="rule-item"><span class="must">MUST:</span> <code>autocomplete</code> + meaningful <code>name</code>; correct <code>type</code> and <code>inputmode</code></li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Disable spellcheck for emails/codes/usernames</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Placeholders end with ellipsis and show example pattern (eg, <code>+1 (123) 456-7890</code>, <code>sk-012345…</code>)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Warn on unsaved changes before navigation</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Compatible with password managers & 2FA; allow pasting one-time codes</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Trim values to handle text expansion trailing spaces</li> | |
| <li class="rule-item"><span class="must">MUST:</span> No dead zones on checkboxes/radios; label+control share one generous hit target</li> | |
| </ul> | |
| <h3>State & Navigation</h3> | |
| <ul> | |
| <li class="rule-item"><span class="must">MUST:</span> URL reflects state (deep-link filters/tabs/pagination/expanded panels) Prefer libs like <a href="https://nuqs.dev" target="_blank" rel="noopener">nuqs</a></li> | |
| <li class="rule-item"><span class="must">MUST:</span> Back/Forward restores scroll</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Links are links—use <code><a>/<Link></code> for navigation (support Cmd/Ctrl/middle-click)</li> | |
| </ul> | |
| <h3>Feedback</h3> | |
| <ul> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Optimistic UI; reconcile on response; on failure show error and rollback or offer Undo</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Confirm destructive actions or provide Undo window</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Use polite <code>aria-live</code> for toasts/inline validation</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Ellipsis (<code>…</code>) for options that open follow-ups (eg, "Rename…") and loading states (eg, "Loading…", "Saving…", "Generating…")</li> | |
| </ul> | |
| <h3>Touch/Drag/Scroll</h3> | |
| <ul> | |
| <li class="rule-item"><span class="must">MUST:</span> Design forgiving interactions (generous targets, clear affordances; avoid finickiness)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Delay first tooltip in a group; subsequent peers no delay</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Intentional <code>overscroll-behavior: contain</code> in modals/drawers</li> | |
| <li class="rule-item"><span class="must">MUST:</span> During drag, disable text selection and set <code>inert</code> on dragged element/containers</li> | |
| <li class="rule-item"><span class="must">MUST:</span> No "dead-looking" interactive zones—if it looks clickable, it is</li> | |
| </ul> | |
| <h3>Autofocus</h3> | |
| <ul> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Autofocus on desktop when there's a single primary input; rarely on mobile (to avoid layout shift)</li> | |
| </ul> | |
| <h2>Animation</h2> | |
| <ul> | |
| <li class="rule-item"><span class="must">MUST:</span> Honor <code>prefers-reduced-motion</code> (provide reduced variant)</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Prefer CSS > Web Animations API > JS libraries</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Animate compositor-friendly props (<code>transform</code>, <code>opacity</code>); avoid layout/repaint props (<code>top/left/width/height</code>)</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Animate only to clarify cause/effect or add deliberate delight</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Choose easing to match the change (size/distance/trigger)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Animations are interruptible and input-driven (avoid autoplay)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Correct <code>transform-origin</code> (motion starts where it "physically" should)</li> | |
| </ul> | |
| <h2>Layout</h2> | |
| <ul> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Optical alignment; adjust by ±1px when perception beats geometry</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Deliberate alignment to grid/baseline/edges/optical centers—no accidental placement</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Balance icon/text lockups (stroke/weight/size/spacing/color)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Verify mobile, laptop, ultra-wide (simulate ultra-wide at 50% zoom)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Respect safe areas (use <code>env(safe-area-inset-*)</code>)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Avoid unwanted scrollbars; fix overflows</li> | |
| </ul> | |
| <h2>Content & Accessibility</h2> | |
| <ul> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Inline help first; tooltips last resort</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Skeletons mirror final content to avoid layout shift</li> | |
| <li class="rule-item"><span class="must">MUST:</span> <code><title></code> matches current context</li> | |
| <li class="rule-item"><span class="must">MUST:</span> No dead ends; always offer next step/recovery</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Design empty/sparse/dense/error states</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Curly quotes (" "); avoid widows/orphans</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Tabular numbers for comparisons (<code>font-variant-numeric: tabular-nums</code> or a mono like Geist Mono)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Redundant status cues (not color-only); icons have text labels</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Don't ship the schema—visuals may omit labels but accessible names still exist</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Use the ellipsis character <code>…</code> (not <code>...</code>)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> <code>scroll-margin-top</code> on headings for anchored links; include a "Skip to content" link; hierarchical <code><h1–h6></code></li> | |
| <li class="rule-item"><span class="must">MUST:</span> Resilient to user-generated content (short/avg/very long)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Locale-aware dates/times/numbers/currency</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Accurate names (<code>aria-label</code>), decorative elements <code>aria-hidden</code>, verify in the Accessibility Tree</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Icon-only buttons have descriptive <code>aria-label</code></li> | |
| <li class="rule-item"><span class="must">MUST:</span> Prefer native semantics (<code>button</code>, <code>a</code>, <code>label</code>, <code>table</code>) before ARIA</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Right-clicking the nav logo surfaces brand assets</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Use non-breaking spaces to glue terms: <code>10 MB</code>, <code>⌘ + K</code>, <code>Vercel SDK</code></li> | |
| </ul> | |
| <h2>Performance</h2> | |
| <ul> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Test iOS Low Power Mode and macOS Safari</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Measure reliably (disable extensions that skew runtime)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Track and minimize re-renders (React DevTools/React Scan)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Profile with CPU/network throttling</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Batch layout reads/writes; avoid unnecessary reflows/repaints</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Mutations (<code>POST/PATCH/DELETE</code>) target <500 ms</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Prefer uncontrolled inputs; make controlled loops cheap (keystroke cost)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Virtualize large lists (eg, <code>virtua</code>)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Preload only above-the-fold images; lazy-load the rest</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Prevent CLS from images (explicit dimensions or reserved space)</li> | |
| </ul> | |
| <h2>Design</h2> | |
| <ul> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Layered shadows (ambient + direct)</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Crisp edges via semi-transparent borders + shadows</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Nested radii: child ≤ parent; concentric</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Hue consistency: tint borders/shadows/text toward bg hue</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Accessible charts (color-blind-friendly palettes)</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Meet contrast—prefer <a href="https://apcacontrast.com/" target="_blank" rel="noopener">APCA</a> over WCAG 2</li> | |
| <li class="rule-item"><span class="must">MUST:</span> Increase contrast on <code>:hover/:active/:focus</code></li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Match browser UI to bg</li> | |
| <li class="rule-item"><span class="should">SHOULD:</span> Avoid gradient banding (use masks when needed)</li> | |
| </ul> | |
| </main> | |
| </body> | |
| </html>""" | |
| return html_content | |
| # ========== Stateless Execution (backward compatible) ========== | |
| def execute_code(request: ExecutionRequest): | |
| """ | |
| Execute code in an isolated ephemeral container (stateless). | |
| This is the original execution method - creates a fresh container, | |
| executes code, and destroys the container immediately. | |
| Note: Requires Docker to be available. On Hugging Face Spaces, Docker-in-Docker | |
| is not supported, so this endpoint will not work. | |
| """ | |
| if not executor: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Code execution requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| try: | |
| logger.info(f"Stateless execution: {request.language} code") | |
| result = executor.execute(request) | |
| return result | |
| except Exception as e: | |
| logger.error(f"Execution failed: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Execution failed: {str(e)}" | |
| ) | |
| # ========== Session Management ========== | |
| def create_session(request: CreateSessionRequest): | |
| """ | |
| Create a new persistent VM-like session. | |
| The session is a long-running container with persistent storage, | |
| supporting file uploads and multiple code executions. | |
| """ | |
| if not session_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| try: | |
| logger.info(f"Creating new session with metadata: {request.metadata}") | |
| session = session_manager.create_session(request) | |
| return session | |
| except RuntimeError as e: | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=str(e) | |
| ) | |
| def list_sessions(): | |
| """List all active sessions""" | |
| if not session_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| sessions = session_manager.list_sessions() | |
| return sessions | |
| def get_session(session_id: str = FastAPIPath(..., description="Session ID")): | |
| """Get session details by ID""" | |
| if not session_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| session = session_manager.get_session(session_id) | |
| if not session: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| # Update file count | |
| if file_manager: | |
| try: | |
| files = file_manager.list_files(session.container_id) | |
| session.files_count = len(files) | |
| except: | |
| pass | |
| return session | |
| def destroy_session(session_id: str = FastAPIPath(..., description="Session ID")): | |
| """Destroy a session and cleanup all resources""" | |
| if not session_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| success = session_manager.destroy_session(session_id) | |
| if not success: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| return {"message": f"Session {session_id} destroyed successfully"} | |
| # ========== File Operations ========== | |
| async def upload_file( | |
| session_id: str = FastAPIPath(..., description="Session ID"), | |
| file: UploadFile = File(..., description="File to upload") | |
| ): | |
| """Upload a file to session workspace""" | |
| if not session_manager or not file_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. File operations require Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| # Get session | |
| session = session_manager.get_session(session_id) | |
| if not session: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| try: | |
| # Read file data | |
| file_data = await file.read() | |
| # Upload to container | |
| result = file_manager.upload_file( | |
| container_id=session.container_id, | |
| filename=file.filename, | |
| file_data=file_data | |
| ) | |
| # Update session activity | |
| session_manager.update_activity(session_id) | |
| return result | |
| except ValueError as e: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail=str(e) | |
| ) | |
| except Exception as e: | |
| logger.error(f"File upload failed: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"File upload failed: {str(e)}" | |
| ) | |
| def list_files(session_id: str = FastAPIPath(..., description="Session ID")): | |
| """List files in session workspace""" | |
| if not session_manager or not file_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. File operations require Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| # Get session | |
| session = session_manager.get_session(session_id) | |
| if not session: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| try: | |
| files = file_manager.list_files(session.container_id) | |
| return files | |
| except Exception as e: | |
| logger.error(f"File listing failed: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"File listing failed: {str(e)}" | |
| ) | |
| def download_file( | |
| session_id: str = FastAPIPath(..., description="Session ID"), | |
| filepath: str = FastAPIPath(..., description="File path relative to workspace") | |
| ): | |
| """Download a file from session workspace""" | |
| if not session_manager or not file_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. File operations require Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| # Get session | |
| session = session_manager.get_session(session_id) | |
| if not session: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| try: | |
| file_data = file_manager.download_file(session.container_id, filepath) | |
| # Determine content type | |
| import mimetypes | |
| content_type, _ = mimetypes.guess_type(filepath) | |
| return Response( | |
| content=file_data, | |
| media_type=content_type or "application/octet-stream", | |
| headers={ | |
| "Content-Disposition": f"attachment; filename={filepath.split('/')[-1]}" | |
| } | |
| ) | |
| except ValueError as e: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=str(e) | |
| ) | |
| except Exception as e: | |
| logger.error(f"File download failed: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"File download failed: {str(e)}" | |
| ) | |
| # ========== Execute in Session ========== | |
| def execute_in_session( | |
| session_id: str = FastAPIPath(..., description="Session ID"), | |
| request: ExecuteInSessionRequest = None | |
| ): | |
| """Execute code in an existing session (persistent state)""" | |
| if not session_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| # Get session | |
| session = session_manager.get_session(session_id) | |
| if not session: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| try: | |
| import time | |
| from docker.errors import DockerException | |
| container = executor.client.containers.get(session.container_id) | |
| runner_config = LanguageRunner.get_runner_config(request.language) | |
| start_time = time.time() | |
| # Execute command in running container | |
| exec_result = container.exec_run( | |
| cmd=runner_config["command"] + [request.code], | |
| workdir=request.working_dir, | |
| demux=True, | |
| stream=False | |
| ) | |
| execution_time = time.time() - start_time | |
| # Parse output | |
| stdout = exec_result.output[0].decode('utf-8', errors='replace') if exec_result.output[0] else "" | |
| stderr = exec_result.output[1].decode('utf-8', errors='replace') if exec_result.output[1] else "" | |
| # Update session activity | |
| session_manager.update_activity(session_id) | |
| return ExecutionResponse( | |
| stdout=stdout, | |
| stderr=stderr, | |
| exit_code=exec_result.exit_code, | |
| execution_time=round(execution_time, 3), | |
| error=None if exec_result.exit_code == 0 else "Execution failed" | |
| ) | |
| except DockerException as e: | |
| logger.error(f"Docker error: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Execution failed: {str(e)}" | |
| ) | |
| def execute_file_in_session( | |
| session_id: str = FastAPIPath(..., description="Session ID"), | |
| request: ExecuteFileRequest = None | |
| ): | |
| """Execute an uploaded file in session""" | |
| if not session_manager: | |
| raise HTTPException( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| detail="Docker is not available. Session management requires Docker which is not supported on this platform. Please deploy to a Docker-capable platform for full functionality." | |
| ) | |
| # Get session | |
| session = session_manager.get_session(session_id) | |
| if not session: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Session {session_id} not found" | |
| ) | |
| try: | |
| import time | |
| container = executor.client.containers.get(session.container_id) | |
| runner_config = LanguageRunner.get_runner_config(request.language) | |
| # Build command based on language | |
| if request.language == Language.PYTHON: | |
| cmd = ["python", request.filepath] + request.args | |
| elif request.language == Language.JAVASCRIPT: | |
| cmd = ["node", request.filepath] + request.args | |
| elif request.language == Language.BASH: | |
| cmd = ["bash", request.filepath] + request.args | |
| else: | |
| cmd = runner_config["command"] + [request.filepath] + request.args | |
| start_time = time.time() | |
| # Execute file | |
| exec_result = container.exec_run( | |
| cmd=cmd, | |
| workdir="/workspace", | |
| demux=True, | |
| stream=False | |
| ) | |
| execution_time = time.time() - start_time | |
| # Parse output | |
| stdout = exec_result.output[0].decode('utf-8', errors='replace') if exec_result.output[0] else "" | |
| stderr = exec_result.output[1].decode('utf-8', errors='replace') if exec_result.output[1] else "" | |
| # Update session activity | |
| session_manager.update_activity(session_id) | |
| return ExecutionResponse( | |
| stdout=stdout, | |
| stderr=stderr, | |
| exit_code=exec_result.exit_code, | |
| execution_time=round(execution_time, 3), | |
| error=None if exec_result.exit_code == 0 else "Execution failed" | |
| ) | |
| except Exception as e: | |
| logger.error(f"File execution failed: {e}", exc_info=True) | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"File execution failed: {str(e)}" | |
| ) | |
| async def global_exception_handler( request, exc): | |
| """Global exception handler""" | |
| logger.error(f"Unhandled exception: {exc}", exc_info=True) | |
| return JSONResponse( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| content={ | |
| "error": "Internal server error", | |
| "detail": str(exc) | |
| } | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run( | |
| "app:app", | |
| host="0.0.0.0", | |
| port=7860, | |
| reload=False, | |
| log_level="info" | |
| ) |