File size: 6,637 Bytes
5b4d3f3 d5332db 5b4d3f3 d5332db 5b4d3f3 d5332db 5b4d3f3 d5332db 5b4d3f3 e8c0934 5b4d3f3 d5332db 5b4d3f3 c87fa3b 5b4d3f3 c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db 5b4d3f3 d5332db 5b4d3f3 d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db e8c0934 d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b d5332db c87fa3b 5b4d3f3 c87fa3b d5332db c87fa3b d5332db 5b4d3f3 d5332db c87fa3b 5b4d3f3 c87fa3b 5b4d3f3 d5332db c87fa3b d5332db 5b4d3f3 d5332db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | 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() |