4 Commits

12 changed files with 377 additions and 134 deletions

View File

@@ -39,12 +39,9 @@ class ASEngineClient:
# 连接事件 # 连接事件
@self.sio.on('connect', namespace=self.namespace) @self.sio.on('connect', namespace=self.namespace)
def on_connect(): def on_connect():
print("---------------ASEngineClient on_connect----------------")
self.sid = self.sio.sid
print(f"Connected to server with sid: {self.sid}")
self.connected = True self.connected = True
self.sio.emit('join', {'id': self.id}, namespace=self.namespace) self.sid = self.sio.get_sid(namespace=self.namespace)
print(f"Connected to server. SID: {self.sid}")
if self._on_connect_callback is not None: if self._on_connect_callback is not None:
self._on_connect_callback( self._on_connect_callback(
self, self,

View File

@@ -14,7 +14,7 @@ from typing import Optional
mongo = PyMongo() mongo = PyMongo()
socketio = SocketIO(cors_allowed_origins="*",max_payload_size=52428800, async_mode="eventlet",always_connect=False) # 不直接传 app socketio = SocketIO(cors_allowed_origins="*",max_payload_size=52428800, async_mode="eventlet") # 不直接传 app
cors = CORS() cors = CORS()
# ===== 你的全局对象 ===== # ===== 你的全局对象 =====
# Backboard # Backboard
@@ -38,7 +38,7 @@ def init_extensions(app):
cors_allowed_origins='*',#app.config["VSCODE_WEB_URL"], cors_allowed_origins='*',#app.config["VSCODE_WEB_URL"],
ping_timeout=app.config["SOCKETIO_PING_TIMEOUT"], ping_timeout=app.config["SOCKETIO_PING_TIMEOUT"],
ping_interval=app.config["SOCKETIO_PING_INTERVAL"], ping_interval=app.config["SOCKETIO_PING_INTERVAL"],
max_payload_size=50000000, async_mode="eventlet",always_connect=False max_payload_size=50000000, async_mode="eventlet"
) )
cors.init_app( cors.init_app(
app, app,

View File

@@ -1,4 +1,6 @@
import requests import requests
from flask_socketio import Namespace
def clear_user_session(asengine_url, asengine_token, user_id): def clear_user_session(asengine_url, asengine_token, user_id):
''' '''
@@ -26,12 +28,9 @@ def on_connect_to_ase(client, entry, init_data, **kwargs):
''' '''
此时刚刚建立好aseclient连接并向前端发送打开code-server-like指令。 此时刚刚建立好aseclient连接并向前端发送打开code-server-like指令。
''' '''
namespace_entry, ase_client, logger = entry namespace_entry, ase_client = entry
logger.info(f"on_connect_to_ase with init_data: {init_data}")
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent') namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent') namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent')
namespace_entry.emit('message', f'{init_data}', room=init_data['room'], namespace='/agent')
restart = init_data.get("restart", False) restart = init_data.get("restart", False)
# 不再需要手动next_chapter因为已经在on_login中根据mongo进度恢复了正确的章节 # 不再需要手动next_chapter因为已经在on_login中根据mongo进度恢复了正确的章节

View File

@@ -93,6 +93,7 @@ def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
# 检查响应 # 检查响应
if response.status_code == 200: if response.status_code == 200:
current_app.logger.info(f"获取MultiAgents对话列表成功: user_id={user_id}, json={response.json()}")
return response.json() return response.json()
else: else:
current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}") current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}")

View File

@@ -63,12 +63,6 @@ class VSCodeNamespace(Namespace):
class AgentNamespace(Namespace): class AgentNamespace(Namespace):
def __init__(self, namespace):
super().__init__(namespace)
# 使用字典存储每个用户的chatmanager和ase_client避免多用户冲突
self.user_chatmanagers = {}
self.user_ase_clients = {}
def _load_learning_progress(self, chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn): def _load_learning_progress(self, chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn):
"""加载并恢复用户学习进度 """加载并恢复用户学习进度
@@ -126,8 +120,8 @@ class AgentNamespace(Namespace):
chapter_name = data['chapter_name'] chapter_name = data['chapter_name']
user_id = uuid2username[user_uuid] user_id = uuid2username[user_uuid]
session['user_uuid'] = user_uuid session['user_uuid'] = user_uuid
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
print(f'User connected with session user_uuid: {user_uuid}') print(f'User connected with session user_uuid: {user_uuid}')
join_room(user_uuid)
user_uuid2chatmanager = ex["user_uuid2chatmanager"] user_uuid2chatmanager = ex["user_uuid2chatmanager"]
user_uuid2ase_client = ex["user_uuid2ase_client"] user_uuid2ase_client = ex["user_uuid2ase_client"]
@@ -158,9 +152,8 @@ class AgentNamespace(Namespace):
if chatmanager.chat_historys: if chatmanager.chat_historys:
emit('messages', chatmanager.chat_historys, room=user_uuid, namespace='/agent') emit('messages', chatmanager.chat_historys, room=user_uuid, namespace='/agent')
# 存储每个用户的chatmanager和ase_client到字典中使用用户UUID作为键 self.chatmanager = chatmanager
self.user_chatmanagers[user_uuid] = chatmanager self.ase_client = user_uuid2ase_client[user_uuid]
self.user_ase_clients[user_uuid] = user_uuid2ase_client[user_uuid]
# 将restart设置为False避免后续重复处理 # 将restart设置为False避免后续重复处理
restart = chatmanager.restart restart = chatmanager.restart
@@ -168,14 +161,14 @@ class AgentNamespace(Namespace):
user_uuid2ase_client[user_uuid].register_on_connect_entry( user_uuid2ase_client[user_uuid].register_on_connect_entry(
callback=on_connect_to_ase, callback=on_connect_to_ase,
entry=(self, user_uuid2ase_client[user_uuid], current_app.logger), entry=(self, user_uuid2ase_client[user_uuid]),
init_data={'room':user_uuid, 'restart':restart} init_data={'room':user_uuid, 'restart':restart}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog', route='dialog',
callback=receive_ase_dialog, callback=receive_ase_dialog,
entry=self, entry=self,
init_data={'room':user_uuid} init_data={'room': user_uuid}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog-hint', route='dialog-hint',
@@ -193,13 +186,13 @@ class AgentNamespace(Namespace):
route='judge', route='judge',
callback=receive_ase_judge, callback=receive_ase_judge,
entry=self, entry=self,
init_data={'room':user_uuid} init_data={'room': user_uuid}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='next_chapter', route='next_chapter',
callback=receive_ase_next_chapter, callback=receive_ase_next_chapter,
entry=self, entry=self,
init_data={'room':user_uuid} init_data={'room': user_uuid}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='chapter_score', route='chapter_score',

View File

@@ -66,23 +66,25 @@ function cleanupExpiredApprovals() {
// 定时清理过期记录 // 定时清理过期记录
setInterval(cleanupExpiredApprovals, 3000); // 每3秒检查一次 setInterval(cleanupExpiredApprovals, 3000); // 每3秒检查一次
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
user_data = window.appData; data = window.appData;
const host = window.location.host; const host = window.location.host;
console.log(`Connecting to WebSocket server at wss://${host}/agent`); console.log(`Connecting to WebSocket server at wss://${host}/agent`);
socket = io(`wss://${host}/agent`, { socket = io(`wss://${host}/agent`, {
query: { query: {
user_uuid: user_data.user_uuid, user_uuid: data.user_uuid,
folder: user_data.folder folder: data.folder
} }
}); });
// 初始化聊天消息管理器 // 初始化聊天消息管理器
chatManager = new ChatMessageManager(); chatManager = new ChatMessageManager();
socket.on('connect', function () { socket.on('connect', function () {
console.log('Connected to WebSocket server'+socket.id+"with user"+JSON.stringify(user_data)); console.log('Connected to server');
socket.emit('login',JSON.stringify(user_data)); socket.emit('login',JSON.stringify(data));
}); });
socket.on('open-iframe', function() { socket.on('open-iframe', function(data) {
OpenIframe(user_data); OpenIframe();
}); });
// 监听来自服务器的消息 // 监听来自服务器的消息
socket.on('voiceMessage', function (data) { socket.on('voiceMessage', function (data) {

View File

@@ -208,7 +208,7 @@
console.error('Error fetching session:', error); console.error('Error fetching session:', error);
}); });
let lastiframe = null; let lastiframe = null;
function OpenIframe(data){ function OpenIframe(){
// 等待1s再加载 // 等待1s再加载
setTimeout(() => { setTimeout(() => {
// 在成功获取 session 后创建并插入 iframe // 在成功获取 session 后创建并插入 iframe

View File

@@ -187,7 +187,7 @@
let lastIframe = null; let lastIframe = null;
function OpenIframe(data){ function OpenIframe(){
// 在成功获取 session 后创建并插入 iframe // 在成功获取 session 后创建并插入 iframe
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
const div=document.getElementById('vscodeWeb') const div=document.getElementById('vscodeWeb')

View File

@@ -91,6 +91,38 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
caddy_conf = f"""
handle_path /vsc{user_id}/* {{
reverse_proxy localhost:{code_server_port}
}}
"""
caddy_config_path = "/etc/caddy/Caddyfile"
with open(caddy_config_path, "r") as f:
lines = f.readlines()
pattern = re.compile(
r'handle_path\s+/vsc' + re.escape(user_id) + r'/\* {.*?reverse_proxy.*?}',
re.DOTALL
)
for i in range(len(lines)):
if (lines[i].strip()==''): lines[i]=''
content = "".join(lines)
if pattern.search(content):
new_content = pattern.sub('', content)
lines = new_content.splitlines(keepends=True)
if len(lines) >= 2:
insert_position = len(lines) - 2
else:
insert_position = len(lines)
if caddy_conf[-1] not in ['\n', '\r']:
caddy_conf += '\n'
lines.insert(insert_position, caddy_conf)
with open(caddy_config_path, "w") as f:
f.writelines(lines)
try:
subprocess.run(["sudo", "caddy", "reload", "--config", "/etc/caddy/Caddyfile"])
except subprocess.CalledProcessError as e:
print(f"Error reload caddy {e}")
cfg = current_app.config["VSCODE_WEB_PATH"] cfg = current_app.config["VSCODE_WEB_PATH"]
path_for_vscode = path_dir path_for_vscode = path_dir

View File

@@ -48,8 +48,9 @@ try:
def custom_excepthook(exc_type, exc_value, exc_traceback): def custom_excepthook(exc_type, exc_value, exc_traceback):
"""全局异常钩子,捕获所有未处理的异常""" """全局异常钩子,捕获所有未处理的异常"""
if exc_type.__name__ in ['OSError', 'IOError'] and 'Bad file descriptor' in str(exc_value): error_str = str(exc_value)
# 处理 Bad file descriptor 错误,不导致服务器宕机 if exc_type.__name__ in ['OSError', 'IOError'] and any(msg in error_str for msg in ['Bad file descriptor', 'Socket operation on non-socket', 'Operation on closed file']):
# 处理各种socket错误不导致服务器宕机
if not is_duplicate_error(exc_value): if not is_duplicate_error(exc_value):
print(f"[Global Error Handler] {exc_type.__name__}: {exc_value}") print(f"[Global Error Handler] {exc_type.__name__}: {exc_value}")
print("[Traceback]") print("[Traceback]")

View File

@@ -3,12 +3,12 @@ from flask_socketio import SocketIO
from .config import Config from .config import Config
# 初始化扩展 # 初始化扩展
cors = CORS() cors = CORS()
socketio = SocketIO(path='vsc-like-ws',cors_allowed_origins="*",max_payload_size=5242880, async_mode="eventlet",always_connect=False) # 不直接传 app socketio = SocketIO(path='vsc-like-ws',cors_allowed_origins="*",max_payload_size=5242880, async_mode="eventlet") # 不直接传 app
session_paths = {} session_paths = {}
def init_extensions(app): def init_extensions(app):
cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
socketio.init_app(app, cors_allowed_origins="*", max_payload_size=5242880, async_mode="eventlet",always_connect=False) socketio.init_app(app, cors_allowed_origins="*", max_payload_size=5242880, async_mode="eventlet")
app.extensions["session_paths"] = session_paths app.extensions["session_paths"] = session_paths
def register_namespaces(app): def register_namespaces(app):

View File

@@ -14,6 +14,7 @@ import termios
import struct import struct
import fcntl import fcntl
import psutil import psutil
TERM_INIT_CONFIG = { TERM_INIT_CONFIG = {
# instead of local server runnning this web terminal service # instead of local server runnning this web terminal service
# "domain" is the target that you want to access through local server (with this web terminal) # "domain" is the target that you want to access through local server (with this web terminal)
@@ -24,9 +25,108 @@ TERM_INIT_CONFIG = {
'ssh': '/usr/bin/ssh' 'ssh': '/usr/bin/ssh'
} }
} }
terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like') terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like')
class TerminalSessionManager:
"""终端会话管理器,用于统一管理所有终端会话"""
def __init__(self):
# 存储所有终端会话key为session_idvalue为会话配置
self.sessions = {}
# 线程锁,保证多线程安全
self.lock = threading.Lock()
# 最大终端数量限制
self.max_terminals = 100
# 终端空闲超时时间(秒)
self.idle_timeout = 3600 # 1小时
def create_session(self):
"""创建新的终端会话"""
with self.lock:
# 检查是否达到最大终端数量
if len(self.sessions) >= self.max_terminals:
return None, "Maximum number of terminals reached"
# 生成唯一的会话ID和房间ID
session_id = str(uuid.uuid4())
room_id = f"terminal_{session_id}"
# 创建会话配置
session_config = {
**TERM_INIT_CONFIG.copy(),
'session_id': session_id,
'room_id': room_id,
'created_at': time.time(),
'last_activity': time.time()
}
# 保存会话
self.sessions[session_id] = session_config
return session_config, None
def get_session(self, session_id):
"""获取会话配置"""
with self.lock:
return self.sessions.get(session_id)
def update_session(self, session_id, updates):
"""更新会话配置"""
with self.lock:
if session_id not in self.sessions:
return False
# 更新会话配置
self.sessions[session_id].update(updates)
# 更新最后活动时间
self.sessions[session_id]['last_activity'] = time.time()
return True
def delete_session(self, session_id):
"""删除会话"""
with self.lock:
if session_id in self.sessions:
del self.sessions[session_id]
return True
return False
def get_session_by_room_id(self, room_id):
"""通过房间ID获取会话"""
with self.lock:
for session_config in self.sessions.values():
if session_config.get('room_id') == room_id:
return session_config
return None
def cleanup_inactive_sessions(self):
"""清理不活动的会话"""
with self.lock:
current_time = time.time()
expired_sessions = []
# 找出超时的会话
for session_id, session_config in self.sessions.items():
if current_time - session_config['last_activity'] > self.idle_timeout:
expired_sessions.append(session_id)
# 删除超时的会话
for session_id in expired_sessions:
del self.sessions[session_id]
return expired_sessions
def get_active_session_count(self):
"""获取当前活动会话数量"""
with self.lock:
return len(self.sessions)
# 创建全局终端会话管理器实例
terminal_manager = TerminalSessionManager()
def set_winsize(fd, row, col, xpix=0, ypix=0): def set_winsize(fd, row, col, xpix=0, ypix=0):
try: try:
@@ -123,53 +223,46 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None, namespace=None)
pass pass
class VSCLikeNameSpace(Namespace): class VSCLikeNameSpace(Namespace):
def on_connect(self): def __init__(self, namespace=None):
"""new client connected""" super().__init__(namespace)
# 确保terminal_config存在于session # 存储客户端socket_id到session_id的映射
if 'terminal_config' not in session: self.socket_session_map = {}
session['terminal_config'] = TERM_INIT_CONFIG.copy() # 线程锁,保证多线程安全
session.modified = True self.lock = threading.Lock()
terminal_config = session['terminal_config'] def create_terminal(self, session_id):
"""创建新的终端并更新会话配置"""
# 获取会话配置
terminal_config = terminal_manager.get_session(session_id)
if not terminal_config:
current_app.logger.error(f"Session not found: {session_id}")
return None
# 检查是否已经有运行中的子进程 room_id = terminal_config['room_id']
if terminal_config.get('child_pid'): current_app.logger.debug(f"Creating new terminal for room: {room_id}")
try:
# 验证进程是否真的存在
child_process = psutil.Process(terminal_config['child_pid'])
if child_process.status() in ('running', 'sleeping'):
current_app.logger.debug("Already running child process: {}".format(terminal_config['child_pid']))
return
except psutil.NoSuchProcess:
# 进程不存在,清理配置
terminal_config.pop('child_pid', None)
terminal_config.pop('fd', None)
session.modified = True
# create child process attached to a pty we can read from and write to # create child process attached to a pty we can read from and write to
(child_pid, fd) = pty.fork() (child_pid, fd) = pty.fork()
lesson_path = session.get('path')
if child_pid == 0: if child_pid == 0:
# this is the child process fork. # this is the child process fork.
# anything printed here will show up in the pty, including the output # anything printed here will show up in the pty, including the output
# of this subprocess # of this subprocess
try: try:
# 获取终端配置 # 获取终端配置
terminal_config = session.get('terminal_config', {})
term_type = terminal_config.get('term_type') term_type = terminal_config.get('term_type')
if not term_type: if not term_type:
print("Terminal type not specified, exit") print("Terminal type not specified, exit")
os._exit(1) os._exit(1)
path = TERM_INIT_CONFIG.get('client_path', {}).get(term_type, None) path = terminal_config.get('client_path', {}).get(term_type, None)
if not path or not os.path.exists(path): if not path or not os.path.exists(path):
print("Can't locate {} binary at {}, exit".format(term_type, path)) print(f"Can't locate {term_type} binary at {path}, exit")
os._exit(1) os._exit(1)
# 获取连接参数 # 获取连接参数
username = terminal_config.get('username') username = terminal_config.get('username')
domain = terminal_config.get('domain', TERM_INIT_CONFIG['domain']) domain = terminal_config.get('domain')
port = terminal_config.get('port') port = terminal_config.get('port')
if not username or not domain: if not username or not domain:
@@ -185,34 +278,70 @@ class VSCLikeNameSpace(Namespace):
elif term_type == 'ssh': elif term_type == 'ssh':
# 使用ssh连接 # 使用ssh连接
ssh_port = port if port else 22 # 默认端口22 ssh_port = port if port else 22 # 默认端口22
os.execl(path, 'ssh', '-p', str(ssh_port), '{}@{}'.format(username, domain)) os.execl(path, 'ssh', '-p', str(ssh_port), f'{username}@{domain}')
else: else:
print("Wrong term type {}, exit".format(term_type)) print(f"Wrong term type {term_type}, exit")
os._exit(1) os._exit(1)
except Exception as e: except Exception as e:
print("Error in child process: {}", str(e)) print(f"Error in child process: {e}")
os._exit(1) os._exit(1)
else: else:
# 更新会话配置 # 更新会话配置,保存到终端管理器
terminal_config['fd'] = fd updates = {
terminal_config['child_pid'] = child_pid 'fd': fd,
terminal_config['room_id'] = rooms()[0] 'child_pid': child_pid
session.modified = True }
terminal_manager.update_session(session_id, updates)
# 设置初始窗口大小 # 设置初始窗口大小
set_winsize(fd, 50, 50) set_winsize(fd, 50, 50)
# 记录日志 # 记录日志
current_app.logger.debug("child pid = {}".format(child_pid)) current_app.logger.debug(f"Child process created: {child_pid} for room: {room_id}")
current_app.logger.debug("rooms of this session = {}".format(rooms()))
# 启动后台任务读取pty输出 # 启动后台任务读取pty输出传入正确的room_id
socketio.start_background_task(read_and_forward_pty_output, fd, child_pid, rooms()[0], self) socketio.start_background_task(read_and_forward_pty_output, fd, child_pid, room_id, self)
current_app.logger.debug("background task running") current_app.logger.debug("Background task running")
return terminal_config
def on_connect(self):
"""new client connected"""
# 创建新的终端会话
terminal_config, error = terminal_manager.create_session()
if not terminal_config:
current_app.logger.error(f"Failed to create terminal session: {error}")
return
session_id = terminal_config['session_id']
room_id = terminal_config['room_id']
# 加入房间
join_room(room_id)
# 保存socket_id到session_id的映射
socket_id = request.sid
with self.lock:
self.socket_session_map[socket_id] = session_id
current_app.logger.debug(f"Client connected, session_id: {session_id}, room_id: {room_id}, socket_id: {socket_id}")
# 创建终端
self.create_terminal(session_id)
current_app.logger.debug("background task running")
def on_pty_input(self, data): def on_pty_input(self, data):
"""write to the child pty, which now is the ssh process from this machine to the 'domain' configured """write to the child pty, which now is the ssh process from this machine to the 'domain' configured
""" """
# 获取socket_id对应的session_id
socket_id = request.sid
with self.lock:
session_id = self.socket_session_map.get(socket_id)
if not session_id:
current_app.logger.error(f"No session found for socket: {socket_id}")
return
# 输入验证 # 输入验证
if not isinstance(data, dict): if not isinstance(data, dict):
current_app.logger.error("Invalid input format: expected dictionary") current_app.logger.error("Invalid input format: expected dictionary")
@@ -229,41 +358,77 @@ class VSCLikeNameSpace(Namespace):
current_app.logger.warning("Input truncated due to length") current_app.logger.warning("Input truncated due to length")
try: try:
terminal_config = session.get('terminal_config', {}) terminal_config = terminal_manager.get_session(session_id)
if not terminal_config:
current_app.logger.error(f"Session not found: {session_id}")
return
child_pid = terminal_config.get('child_pid') child_pid = terminal_config.get('child_pid')
# 如果没有子进程,重新创建终端
if not child_pid: if not child_pid:
current_app.logger.error("No child process found") current_app.logger.debug(f"No child process found for session: {session_id}, creating new terminal")
return terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for session: {session_id}")
return
child_process = psutil.Process(child_pid) # 检查子进程是否存在且运行中
if child_process.status() not in ('running', 'sleeping'): try:
current_app.logger.debug("Child process not running, cleaning up") child_process = psutil.Process(child_pid)
terminal_config.pop('child_pid', None) if child_process.status() not in ('running', 'sleeping'):
terminal_config.pop('fd', None) current_app.logger.debug(f"Child process not running for session: {session_id}, creating new terminal")
session.modified = True terminal_config = self.create_terminal(session_id)
return if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for session: {session_id}")
return
except psutil.NoSuchProcess:
current_app.logger.debug(f"Child process not found for session: {session_id}, creating new terminal")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for session: {session_id}")
return
# 获取文件描述符并写入数据
fd = terminal_config.get('fd') fd = terminal_config.get('fd')
if fd: if fd:
try: try:
os.write(fd, input_data.encode()) os.write(fd, input_data.encode())
except (OSError, IOError) as e: except (OSError, IOError) as e:
# File descriptor closed or invalid, clean up # File descriptor closed or invalid, create new terminal
current_app.logger.debug(f"Error writing to file descriptor: {e}") current_app.logger.debug(f"Error writing to file descriptor: {e}, creating new terminal for session: {session_id}")
terminal_config.pop('child_pid', None) self.create_terminal(session_id)
terminal_config.pop('fd', None)
session.modified = True
except psutil.NoSuchProcess:
# Process no longer exists, clean up
terminal_config = session.get('terminal_config', {})
terminal_config.pop('child_pid', None)
terminal_config.pop('fd', None)
session.modified = True
except Exception as e: except Exception as e:
current_app.logger.error(f"Error in pty_input handling: {e}") current_app.logger.error(f"Error in pty_input handling: {e}")
# 发生异常时尝试重新创建终端
try:
self.create_terminal(session_id)
except Exception as create_err:
current_app.logger.error(f"Failed to recreate terminal: {create_err}")
def on_resize(self, data): def on_resize(self, data):
# 获取socket_id对应的session_id
socket_id = request.sid
with self.lock:
session_id = self.socket_session_map.get(socket_id)
if not session_id:
current_app.logger.error(f"No session found for socket: {socket_id}")
return
# 输入验证 # 输入验证
if not isinstance(data, dict): if not isinstance(data, dict):
current_app.logger.error("Invalid resize data format: expected dictionary") current_app.logger.error("Invalid resize data format: expected dictionary")
@@ -283,53 +448,101 @@ class VSCLikeNameSpace(Namespace):
return return
try: try:
terminal_config = session.get('terminal_config', {}) terminal_config = terminal_manager.get_session(session_id)
child_pid = terminal_config.get('child_pid') if not terminal_config:
if not child_pid: current_app.logger.error(f"Session not found: {session_id}")
current_app.logger.error("No child process found for resize")
return return
child_pid = terminal_config.get('child_pid')
# 如果没有子进程,重新创建终端
if not child_pid:
current_app.logger.debug(f"No child process found for resize, creating new terminal for session: {session_id}")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for resize, session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for resize, session: {session_id}")
return
# 检查子进程是否存在且运行中 # 检查子进程是否存在且运行中
child_process = psutil.Process(child_pid) try:
if child_process.status() not in ('running', 'sleeping'): child_process = psutil.Process(child_pid)
current_app.logger.debug("Child process not running, cleaning up resize") if child_process.status() not in ('running', 'sleeping'):
terminal_config.pop('child_pid', None) current_app.logger.debug(f"Child process not running for resize, creating new terminal for session: {session_id}")
terminal_config.pop('fd', None) terminal_config = self.create_terminal(session_id)
session.modified = True if not terminal_config:
return current_app.logger.error(f"Failed to create new terminal for resize, session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for resize, session: {session_id}")
return
except psutil.NoSuchProcess:
current_app.logger.debug(f"Child process not found for resize, creating new terminal for session: {session_id}")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for resize, session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for resize, session: {session_id}")
return
# 获取文件描述符并调整窗口大小
fd = terminal_config.get('fd') fd = terminal_config.get('fd')
if fd: if fd:
# 检查文件描述符是否有效
try: try:
os.fstat(fd) os.fstat(fd)
set_winsize(fd, rows, cols) set_winsize(fd, rows, cols)
except (OSError, IOError) as e: except (OSError, IOError) as e:
# 文件描述符无效,清理资源 # 文件描述符无效,创建新终端
current_app.logger.debug(f"Error resizing terminal: {e}") current_app.logger.debug(f"Error resizing terminal: {e}, creating new terminal for session: {session_id}")
terminal_config.pop('child_pid', None) self.create_terminal(session_id)
terminal_config.pop('fd', None)
session.modified = True
except psutil.NoSuchProcess:
# 进程不存在,清理资源
terminal_config = session.get('terminal_config', {})
terminal_config.pop('child_pid', None)
terminal_config.pop('fd', None)
session.modified = True
except Exception as e: except Exception as e:
current_app.logger.error(f"Error in resize handling: {e}") current_app.logger.error(f"Error in resize handling: {e}")
# 发生异常时尝试重新创建终端
try:
self.create_terminal(session_id)
except Exception as create_err:
current_app.logger.error(f"Failed to recreate terminal for resize: {create_err}")
def on_disconnect(self): def on_disconnect(self):
terminal_config = session.get('terminal_config', {}) # 获取socket_id对应的session_id
socket_id = request.sid
with self.lock:
session_id = self.socket_session_map.pop(socket_id, None)
if not session_id:
current_app.logger.error(f"No session found for socket: {socket_id}")
return
# 获取会话配置
terminal_config = terminal_manager.get_session(session_id)
if not terminal_config:
current_app.logger.error(f"Session not found: {session_id}")
return
child_pid = terminal_config.get('child_pid') child_pid = terminal_config.get('child_pid')
fd = terminal_config.get('fd') fd = terminal_config.get('fd')
room_id = terminal_config.get('room_id')
current_app.logger.debug(f"Client disconnecting, session_id: {session_id}, room_id: {room_id}, socket_id: {socket_id}")
# 退出房间
if room_id:
leave_room(room_id)
current_app.logger.debug(f"Client left room: {room_id}")
# 先关闭文件描述符,避免其他操作尝试使用无效的文件描述符 # 先关闭文件描述符,避免其他操作尝试使用无效的文件描述符
if fd: if fd:
try: try:
os.close(fd) os.close(fd)
except Exception: current_app.logger.debug(f"Closed file descriptor: {fd}")
pass except Exception as e:
current_app.logger.debug(f"Error closing file descriptor: {e}")
if child_pid: if child_pid:
try: try:
@@ -340,23 +553,28 @@ class VSCLikeNameSpace(Namespace):
child_process.terminate() child_process.terminate()
# Wait for the process to terminate and collect its exit status # Wait for the process to terminate and collect its exit status
child_process.wait(timeout=2) child_process.wait(timeout=2)
current_app.logger.debug('user left the pty alone, terminated and waited') current_app.logger.debug(f'User left pty alone, terminated and waited: {child_pid}')
except psutil.NoSuchProcess as err: except psutil.NoSuchProcess:
# Process already terminated, try to wait anyway to clean up any zombie # Process already terminated, try to wait anyway to clean up any zombie
try: try:
os.waitpid(child_pid, os.WNOHANG) os.waitpid(child_pid, os.WNOHANG)
except Exception: current_app.logger.debug(f'Cleaned up zombie process: {child_pid}')
pass except Exception as e:
current_app.logger.debug(f"Error cleaning up zombie process: {e}")
except psutil.TimeoutExpired: except psutil.TimeoutExpired:
# If process didn't terminate in time, kill it forcefully # If process didn't terminate in time, kill it forcefully
try: try:
child_process.kill() child_process.kill()
child_process.wait(timeout=1) child_process.wait(timeout=1)
except Exception: current_app.logger.debug(f'Forcefully killed process: {child_pid}')
pass except Exception as e:
current_app.logger.debug(f"Error forcefully killing process: {e}")
except Exception as err: except Exception as err:
current_app.logger.error(f'Error terminating process: {err}') current_app.logger.error(f'Error terminating process: {err}')
# Reset session config # 从终端管理器中删除会话
session['terminal_config'] = TERM_INIT_CONFIG terminal_manager.delete_session(session_id)
current_app.logger.debug(f"Session deleted: {session_id}")
current_app.logger.debug(f"Client disconnected, session_id: {session_id}, room_id: {room_id}")
current_app.logger.debug('Client disconnected') current_app.logger.debug('Client disconnected')