Ub / app.py
jyujiaf's picture
Update app.py
c87fa3b verified
import os
import subprocess
import asyncio
from aiohttp import web
# 配置
TTYD_PORT = 7681
SERVER_PORT = 7860
# 简单的 HTML 前端
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:
# 连接到本地运行的 ttyd
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:
# ttyd 期望字节流,将文本编码为 bytes
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:
# 读取 ttyd 的输出
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):
# 启动 ttyd 子进程
# 使用 --writable 确保可写 (虽然默认通常是可写的,但显式指定更安全)
# 注意:如果之前的 ttyd 版本不支持某些参数,这里只保留最基础的 -p
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}...")
# print=None 禁止 aiohttp 自带的日志,避免干扰
web.run_app(app, host='0.0.0.0', port=SERVER_PORT, print=lambda x: None)
if __name__ == '__main__':
main()