import os import select import time import uuid import subprocess import threading from flask import Blueprint, request, current_app, session from flask_socketio import Namespace, join_room, leave_room, emit, SocketIO from threading import Thread import sys from ..extensions import socketio terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like') # 存储终端进程 command_dict = {} terminal_busy = {} terminals = {} terminal_locks = {} def check_process_running(command_name): try: # 使用 pgrep 查找与给定命令相关的进程 result = subprocess.run( ["pgrep", "-f", command_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) return result.returncode == 0 # 如果返回值为 0,说明命令仍在运行 except Exception as e: print(f"Error checking process: {e}") return False def monitor_process(socket_id, command_name, namespace): while True: is_running = check_process_running(command_name) if is_running: print(f"Process '{command_name}' is still running.") else: print(f"Process '{command_name}' is not running.") break # 停止检查,如果进程不再运行 time.sleep(0.1) # 每 0.1 秒检查一次 command_dict[socket_id] = '' terminal_busy[socket_id] = False namespace.emit('terminal_output',{'data': '>'}) def start_terminal_process(socket_id, namespace): """ 监听多个子进程的输出,并通过 WebSocket 发给前端 使用 select 进行非阻塞的 I/O 操作,并实现动态流量控制 """ process = terminals[socket_id] # 获取子进程的 stdout 和 stderr stdout = process.stdout stdin = process.stdin # 将 stdout 和 stderr 添加到 select 监听的文件描述符列表中 # 初始等待时间 timeout = 0.1 max_timeout = 2 # 设置最大超时时间为 2 秒 while True: while True: output = stdout.readline() if not output: break timeout = 0.1 namespace.emit('terminal_output', {'data': output+'\r'}, room=socket_id) timeout = min(timeout * 2, max_timeout) time.sleep(timeout) if process.poll() is not None: break class VSCLikeNameSpace(Namespace): def on_connect(self, data): """ 处理 WebSocket 连接 - 验证会话 - 使用指定的用户启动 bash 终端 - 启动监听子进程输出的线程 """ socket_id = session.get('uuid') user_id = session.get('user_id') # 从 session 获取用户 ID path = session.get('path') if not path or not user_id: emit('error', {'message': 'Authentication failed, no path or user found for this session'}) return False # 拒绝连接 # 使用 sudo 启动终端进程,以指定的 user_id 用户身份执行 try: process = subprocess.Popen( ['sudo', '-u', user_id, 'bash', '-il'], # 使用指定的 user_id 启动 bash stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, # 确保输出是文本格式 ) terminals[socket_id] = process terminal_busy[socket_id] = False emit('connected', {'message': 'OK'}) join_room(socket_id, namespace='/vsc-like') socketio.start_background_task(target=start_terminal_process, socket_id=socket_id, namespace=self) except Exception as e: emit('error', {'message': f'Error starting terminal process: {str(e)}'}) return False def on_send_input(self, data): socket_id = session.get('uuid') input_data = data.get('input') if socket_id not in command_dict:command_dict[socket_id]='' # 检查当前 socket_id 是否有对应的终端 if socket_id in terminals: # 获取该终端的锁进行线程同步 lock = terminal_locks.get(socket_id) if not lock: lock = threading.Lock() terminal_locks[socket_id] = lock with lock: # 在锁的保护下操作终端 try: terminals[socket_id].stdin.write(input_data) command_dict[socket_id]+=input_data if input_data=='\r': terminals[socket_id].stdin.flush() emit('terminal_output', {'data': f'\r\n'}, room=socket_id) if terminal_busy[socket_id]==False: terminal_busy[socket_id] = True socketio.start_background_task(monitor_process, socket_id, command_dict[socket_id], namespace=self) elif input_data=='\x03': terminals[socket_id].stdin.flush() emit('terminal_output', {'data': f'^C\x03\r\n'}, room=socket_id) else: emit('terminal_output', {'data': input_data}, room=socket_id) except Exception as e: emit('error', {'message': f'Error sending input: {str(e)}'}, room=socket_id) else: emit('error', {'message': 'No terminal found for this session'}, room=socket_id) def on_disconnect(): socket_id = session.get('uuid') # 检查是否有终端进程需要终止 if socket_id in terminals: try: # 终止终端进程 terminals[socket_id].terminate() del terminals[socket_id] # 删除锁 if socket_id in terminal_locks: del terminal_locks[socket_id] emit('terminal_output', {'data': 'Terminal disconnected and terminated'}, room=socket_id) except Exception as e: emit('error', {'message': f'Error disconnecting terminal: {str(e)}'}, room=socket_id)