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()