| import os |
| import subprocess |
| import asyncio |
| from aiohttp import web |
|
|
| |
| TTYD_PORT = 7681 |
| SERVER_PORT = 7860 |
|
|
| |
| HTML_CONTENT = """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <title>HF Space Terminal</title> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" /> |
| <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script> |
| <style> |
| body { margin: 0; background: #1e1e1e; overflow: hidden; } |
| #terminal { height: 100vh; width: 100vw; } |
| #status { position: absolute; top: 10px; right: 10px; color: #0f0; background: rgba(0,0,0,0.7); padding: 5px 10px; border-radius: 4px; font-family: sans-serif; font-size: 12px; pointer-events: none; z-index: 999; } |
| </style> |
| </head> |
| <body> |
| <div id="status">Initializing...</div> |
| <div id="terminal"></div> |
| <script> |
| const term = new Terminal({ |
| cursorBlink: true, |
| theme: { background: '#1e1e1e', foreground: '#ffffff' }, |
| fontSize: 14, |
| fontFamily: 'Menlo, Monaco, "Courier New", monospace' |
| }); |
| const fitAddon = new FitAddon.FitAddon(); |
| term.loadAddon(fitAddon); |
| term.open(document.getElementById('terminal')); |
| fitAddon.fit(); |
| |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| const wsUrl = `${protocol}//${window.location.host}/ws`; |
| |
| let sock = null; |
| let reconnectTimer = null; |
| |
| function connect() { |
| if (reconnectTimer) clearTimeout(reconnectTimer); |
| document.getElementById('status').innerText = "Connecting..."; |
| |
| try { |
| sock = new WebSocket(wsUrl); |
| |
| sock.onopen = () => { |
| document.getElementById('status').innerText = "Connected (Ready)"; |
| document.getElementById('status').style.color = "#0f0"; |
| term.write('\\r\\n\\x1b[32m>>> HF Space Ubuntu Terminal Ready <<<\\x1b[0m\\r\\n'); |
| term.write('\\r\\nType commands (e.g., ls, g++, vim)...\\r\\n\\r\\n'); |
| }; |
| |
| term.onData(data => { |
| if (sock && sock.readyState === WebSocket.OPEN) { |
| // 直接发送字符串,aiohttp 会自动处理 |
| sock.send(data); |
| } |
| }); |
| |
| sock.onmessage = event => { |
| // 处理二进制或文本数据 |
| if (event.data instanceof ArrayBuffer) { |
| term.write(new Uint8Array(event.data)); |
| } else { |
| term.write(event.data); |
| } |
| }; |
| |
| sock.onclose = () => { |
| document.getElementById('status').innerText = "Disconnected. Reconnecting..."; |
| document.getElementById('status').style.color = "#ffa500"; |
| reconnectTimer = setTimeout(connect, 2000); |
| }; |
| |
| sock.onerror = () => { |
| document.getElementById('status').innerText = "Connection Error."; |
| document.getElementById('status').style.color = "#ff0000"; |
| }; |
| } catch (e) { |
| document.getElementById('status').innerText = "Error: " + e.message; |
| } |
| } |
| |
| connect(); |
| window.addEventListener('resize', () => fitAddon.fit()); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| async def handle_http(request): |
| return web.Response(text=HTML_CONTENT, content_type='text/html') |
|
|
| async def handle_ws(request): |
| ws = web.WebSocketResponse(heartbeat=30) |
| await ws.prepare(request) |
| |
| reader = None |
| writer = None |
| |
| try: |
| |
| reader, writer = await asyncio.open_connection('127.0.0.1', TTYD_PORT) |
| print("Connected to ttyd backend") |
| except Exception as e: |
| print(f"Failed to connect to ttyd: {e}") |
| await ws.close() |
| return ws |
|
|
| async def ws_to_tty(): |
| try: |
| async for msg in ws: |
| if msg.type == web.WSMsgType.BINARY: |
| writer.write(msg.data) |
| elif msg.type == web.WSMsgType.TEXT: |
| |
| writer.write(msg.data.encode('utf-8')) |
| await writer.drain() |
| except Exception as e: |
| print(f"WS->TTY Error: {e}") |
| finally: |
| if writer: |
| writer.close() |
| try: |
| await writer.wait_closed() |
| except: |
| pass |
|
|
| async def tty_to_ws(): |
| try: |
| while True: |
| |
| data = await reader.read(4096) |
| if not data: |
| break |
| |
| await ws.send_bytes(data) |
| except Exception as e: |
| print(f"TTY->WS Error: {e}") |
| finally: |
| if writer: |
| writer.close() |
| try: |
| await writer.wait_closed() |
| except: |
| pass |
|
|
| |
| await asyncio.gather(ws_to_tty(), tty_to_ws()) |
| |
| if not ws.closed: |
| await ws.close() |
| |
| return ws |
|
|
| async def on_startup(app): |
| |
| |
| |
| cmd = ["/usr/local/bin/ttyd", "-p", str(TTYD_PORT), "/bin/bash"] |
| print(f"Starting ttyd: {' '.join(cmd)}") |
| |
| |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| app['ttyd_proc'] = proc |
| print("ttyd started successfully.") |
|
|
| def main(): |
| app = web.Application() |
| app.router.add_get('/', handle_http) |
| app.router.add_get('/ws', handle_ws) |
| app.on_startup.append(on_startup) |
| |
| print(f"Starting server on port {SERVER_PORT}...") |
| |
| web.run_app(app, host='0.0.0.0', port=SERVER_PORT, print=lambda x: None) |
|
|
| if __name__ == '__main__': |
| main() |