Spaces:
Runtime error
Runtime error
Create remote_client.py
Browse files- remote_client.py +231 -0
remote_client.py
ADDED
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import socketio
|
3 |
+
import subprocess
|
4 |
+
import json
|
5 |
+
import logging
|
6 |
+
import sys
|
7 |
+
import base64
|
8 |
+
import os
|
9 |
+
import threading
|
10 |
+
import time
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# 配置日志
|
14 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
15 |
+
logger = logging.getLogger(__name__)
|
16 |
+
|
17 |
+
class RemoteClient:
|
18 |
+
def __init__(self, space_id, user_token, server_url):
|
19 |
+
self.space_id = space_id
|
20 |
+
self.user_token = user_token
|
21 |
+
self.server_url = server_url
|
22 |
+
self.sio = socketio.AsyncClient(ping_timeout=60, ping_interval=25)
|
23 |
+
self.connected = False
|
24 |
+
|
25 |
+
# 绑定事件
|
26 |
+
self.sio.on('connect', self.on_connect)
|
27 |
+
self.sio.on('disconnect', self.on_disconnect)
|
28 |
+
self.sio.on('registered', self.on_registered)
|
29 |
+
self.sio.on('execute_command', self.on_execute_command)
|
30 |
+
self.sio.on('error', self.on_error)
|
31 |
+
self.sio.on('upload_success', self.on_upload_success)
|
32 |
+
|
33 |
+
async def on_connect(self):
|
34 |
+
logger.info("Connected to server")
|
35 |
+
# 注册客户端
|
36 |
+
await self.sio.emit('register_client', {
|
37 |
+
'space_id': self.space_id,
|
38 |
+
'token': self.user_token
|
39 |
+
})
|
40 |
+
|
41 |
+
async def on_disconnect(self):
|
42 |
+
logger.info("Disconnected from server")
|
43 |
+
self.connected = False
|
44 |
+
|
45 |
+
async def on_registered(self, data):
|
46 |
+
logger.info(f"Successfully registered: {data}")
|
47 |
+
self.connected = True
|
48 |
+
|
49 |
+
async def on_error(self, data):
|
50 |
+
logger.error(f"Server error: {data}")
|
51 |
+
|
52 |
+
async def on_upload_success(self, data):
|
53 |
+
logger.info(f"File upload successful: {data}")
|
54 |
+
|
55 |
+
async def on_execute_command(self, data):
|
56 |
+
"""处理执行命令请求"""
|
57 |
+
task_id = data.get('task_id')
|
58 |
+
command = data.get('command')
|
59 |
+
token = data.get('token')
|
60 |
+
|
61 |
+
logger.info(f"Received command for task {task_id}: {command}")
|
62 |
+
|
63 |
+
# 发送开始执行的状态
|
64 |
+
await self.sio.emit('command_result', {
|
65 |
+
'task_id': task_id,
|
66 |
+
'status': 'running',
|
67 |
+
'output': f'开始执行命令: {command}'
|
68 |
+
})
|
69 |
+
|
70 |
+
# 在后台执行命令
|
71 |
+
asyncio.create_task(self.execute_command_async(task_id, command, token))
|
72 |
+
|
73 |
+
async def execute_command_async(self, task_id, command, token):
|
74 |
+
"""异步执行命令"""
|
75 |
+
try:
|
76 |
+
# 创建 output 目录
|
77 |
+
os.makedirs('./output', exist_ok=True)
|
78 |
+
|
79 |
+
logger.info(f"Executing: {command}")
|
80 |
+
|
81 |
+
# 执行命令
|
82 |
+
process = await asyncio.create_subprocess_shell(
|
83 |
+
command,
|
84 |
+
stdout=asyncio.subprocess.PIPE,
|
85 |
+
stderr=asyncio.subprocess.STDOUT,
|
86 |
+
text=True
|
87 |
+
)
|
88 |
+
|
89 |
+
# 实时读取输出
|
90 |
+
output_buffer = ""
|
91 |
+
while True:
|
92 |
+
line = await process.stdout.readline()
|
93 |
+
if not line:
|
94 |
+
break
|
95 |
+
line_str = line.decode() if isinstance(line, bytes) else line
|
96 |
+
output_buffer += line_str
|
97 |
+
logger.info(f"Command output: {line_str.strip()}")
|
98 |
+
|
99 |
+
# 发送实时输出
|
100 |
+
await self.sio.emit('command_result', {
|
101 |
+
'task_id': task_id,
|
102 |
+
'status': 'running',
|
103 |
+
'output': line_str
|
104 |
+
})
|
105 |
+
|
106 |
+
# 等待进程完成
|
107 |
+
await process.wait()
|
108 |
+
|
109 |
+
if process.returncode == 0:
|
110 |
+
logger.info("Command executed successfully")
|
111 |
+
|
112 |
+
# 扫描 output 目录并上传文件
|
113 |
+
await self.upload_output_files(token)
|
114 |
+
|
115 |
+
await self.sio.emit('command_result', {
|
116 |
+
'task_id': task_id,
|
117 |
+
'status': 'completed',
|
118 |
+
'output': '命令执行完成,文件已上传到网盘'
|
119 |
+
})
|
120 |
+
else:
|
121 |
+
logger.error(f"Command failed with return code: {process.returncode}")
|
122 |
+
await self.sio.emit('command_result', {
|
123 |
+
'task_id': task_id,
|
124 |
+
'status': 'failed',
|
125 |
+
'output': f'命令执行失败,退出码: {process.returncode}'
|
126 |
+
})
|
127 |
+
|
128 |
+
except Exception as e:
|
129 |
+
logger.error(f"Error executing command: {e}")
|
130 |
+
await self.sio.emit('command_result', {
|
131 |
+
'task_id': task_id,
|
132 |
+
'status': 'failed',
|
133 |
+
'output': f'执行命令时出错: {str(e)}'
|
134 |
+
})
|
135 |
+
|
136 |
+
async def upload_output_files(self, token):
|
137 |
+
"""上传 output 目录中的文件"""
|
138 |
+
output_dir = Path('./output')
|
139 |
+
if not output_dir.exists():
|
140 |
+
logger.warning("Output directory does not exist")
|
141 |
+
return
|
142 |
+
|
143 |
+
uploaded_count = 0
|
144 |
+
failed_count = 0
|
145 |
+
|
146 |
+
for file_path in output_dir.iterdir():
|
147 |
+
if file_path.is_file():
|
148 |
+
try:
|
149 |
+
logger.info(f"Uploading file: {file_path.name}")
|
150 |
+
|
151 |
+
# 读取文件内容
|
152 |
+
with open(file_path, 'rb') as f:
|
153 |
+
file_content = f.read()
|
154 |
+
|
155 |
+
# 编码为 base64
|
156 |
+
file_b64 = base64.b64encode(file_content).decode('utf-8')
|
157 |
+
|
158 |
+
# 发送文件上传请求
|
159 |
+
await self.sio.emit('file_upload', {
|
160 |
+
'filename': file_path.name,
|
161 |
+
'content': file_b64,
|
162 |
+
'token': token
|
163 |
+
})
|
164 |
+
|
165 |
+
uploaded_count += 1
|
166 |
+
logger.info(f"Successfully uploaded: {file_path.name}")
|
167 |
+
|
168 |
+
# 删除已上传的文件
|
169 |
+
file_path.unlink()
|
170 |
+
|
171 |
+
# 避免发送过大的数据包,稍作延迟
|
172 |
+
await asyncio.sleep(0.5)
|
173 |
+
|
174 |
+
except Exception as e:
|
175 |
+
logger.error(f"Failed to upload {file_path.name}: {e}")
|
176 |
+
failed_count += 1
|
177 |
+
|
178 |
+
logger.info(f"Upload completed. Success: {uploaded_count}, Failed: {failed_count}")
|
179 |
+
|
180 |
+
async def connect(self):
|
181 |
+
"""连接到服务器"""
|
182 |
+
try:
|
183 |
+
# 构建 WebSocket URL
|
184 |
+
ws_url = self.server_url.replace('https://', 'wss://').replace('http://', 'ws://')
|
185 |
+
if not ws_url.endswith('/'):
|
186 |
+
ws_url += '/'
|
187 |
+
ws_url += 'socket.io/'
|
188 |
+
|
189 |
+
logger.info(f"Connecting to {ws_url}")
|
190 |
+
await self.sio.connect(ws_url, transports=['websocket'])
|
191 |
+
|
192 |
+
# 保持连接
|
193 |
+
while self.connected:
|
194 |
+
await asyncio.sleep(1)
|
195 |
+
|
196 |
+
except Exception as e:
|
197 |
+
logger.error(f"Connection failed: {e}")
|
198 |
+
raise
|
199 |
+
|
200 |
+
async def disconnect(self):
|
201 |
+
"""断开连接"""
|
202 |
+
if self.sio.connected:
|
203 |
+
await self.sio.disconnect()
|
204 |
+
|
205 |
+
async def main():
|
206 |
+
if len(sys.argv) != 4:
|
207 |
+
print("Usage: python3 remote_client.py <space_id> <user_token> <server_url>")
|
208 |
+
print("Example: python3 remote_client.py 'abc123' 'token456' 'https://gkbtyo-rqvays-5001.preview.cloudstudio.work'")
|
209 |
+
sys.exit(1)
|
210 |
+
|
211 |
+
space_id = sys.argv[1]
|
212 |
+
user_token = sys.argv[2]
|
213 |
+
server_url = sys.argv[3]
|
214 |
+
|
215 |
+
logger.info(f"Starting remote client for Space: {space_id}")
|
216 |
+
logger.info(f"Server: {server_url}")
|
217 |
+
|
218 |
+
client = RemoteClient(space_id, user_token, server_url)
|
219 |
+
|
220 |
+
try:
|
221 |
+
await client.connect()
|
222 |
+
except KeyboardInterrupt:
|
223 |
+
logger.info("Received interrupt signal")
|
224 |
+
except Exception as e:
|
225 |
+
logger.error(f"Client error: {e}")
|
226 |
+
finally:
|
227 |
+
await client.disconnect()
|
228 |
+
logger.info("Client stopped")
|
229 |
+
|
230 |
+
if __name__ == "__main__":
|
231 |
+
asyncio.run(main())
|