3 Commits

3 changed files with 121 additions and 38 deletions

View File

@@ -1,5 +1,32 @@
import eventlet import eventlet
eventlet.monkey_patch() # 进行猴子补丁操作 import logging
# 配置 eventlet 日志,抑制 IOClosed 等错误日志
logging.getLogger('eventlet.hubs').setLevel(logging.CRITICAL)
logging.getLogger('eventlet.greenio').setLevel(logging.CRITICAL)
# 进行猴子补丁操作
eventlet.monkey_patch()
# 配置 eventlet hub 以更优雅地处理 IOClosed 错误
try:
from eventlet.hubs import hub
# 保存原始的 handle_error 方法
original_handle_error = hub.Hub.handle_error
def custom_handle_error(self, context, type, value, tb):
# 忽略 IOClosed 错误
if type.__name__ == 'IOClosed' and 'Operation on closed file' in str(value):
return
# 其他错误调用原始方法
original_handle_error(self, context, type, value, tb)
# 替换原始方法
hub.Hub.handle_error = custom_handle_error
except Exception as e:
# 如果修改失败,忽略错误
pass
from flask import Flask from flask import Flask
from .views.routes import main_bp from .views.routes import main_bp
from .views.file import file_bp from .views.file import file_bp

View File

@@ -29,8 +29,12 @@ terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like')
def set_winsize(fd, row, col, xpix=0, ypix=0): def set_winsize(fd, row, col, xpix=0, ypix=0):
winsize = struct.pack("HHHH", row, col, xpix, ypix) try:
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) winsize = struct.pack("HHHH", row, col, xpix, ypix)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
except (OSError, IOError):
# File descriptor closed or invalid, do nothing
pass
def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None): def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
@@ -67,21 +71,35 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
return return
if fd: if fd:
timeout_sec = 0 timeout_sec = 0
(data_ready, _, _) = select.select([fd], [], [], timeout_sec) try:
if data_ready: # Check if file descriptor is still valid by trying to select on it
# output = os.read(fd, max_read_bytes).decode('ascii') (data_ready, _, _) = select.select([fd], [], [], timeout_sec)
timeout=0.1 if data_ready:
try: timeout=0.1
output = os.read(fd, max_read_bytes).decode() try:
except Exception as err: output = os.read(fd, max_read_bytes).decode()
output = """ except (OSError, IOError, EOFError) as err:
***AQUI WEB TERM ERR*** # File descriptor closed or other IO error, exit gracefully
{} return
*********************** except UnicodeDecodeError as err:
""".format(err) output = """
# the key for different visitor to get different terminal (instead of mixing up) ***AQUI WEB TERM ERR***
# is to let the background task push pty response to each one's own (default) ROOM! Unicode decode error: {}
namespace.emit("pty_output", {"output": output}, room=room_id) ***********************
""".format(err)
# the key for different visitor to get different terminal (instead of mixing up)
# is to let the background task push pty response to each one's own (default) ROOM!
try:
namespace.emit("pty_output", {"output": output}, room=room_id)
except Exception:
# If emit fails, the client is probably disconnected, exit
return
except (OSError, IOError) as err:
# File descriptor closed or invalid, exit gracefully
return
except Exception as e:
# Catch any other unexpected exceptions to prevent server crash
pass
finally: finally:
# Clean up file descriptor if it's open # Clean up file descriptor if it's open
if fd: if fd:
@@ -158,7 +176,13 @@ class VSCLikeNameSpace(Namespace):
if fd: if fd:
# print("writing to ptd: %s" % data["input"]) # print("writing to ptd: %s" % data["input"])
# os.write(fd, data["input"].encode('ascii')) # os.write(fd, data["input"].encode('ascii'))
os.write(fd, data["input"].encode()) try:
os.write(fd, data["input"].encode())
except (OSError, IOError):
# File descriptor closed or invalid, clean up
disconnect()
session['terminal_config'] = TERM_INIT_CONFIG
return
def on_resize(self, data): def on_resize(self, data):
@@ -174,10 +198,29 @@ class VSCLikeNameSpace(Namespace):
return return
fd = session.get('terminal_config').get('fd') fd = session.get('terminal_config').get('fd')
if fd: if fd:
set_winsize(fd, data["rows"], data["cols"]) # 检查文件描述符是否有效
try:
# 尝试一个简单的操作来检查文件描述符是否有效
os.fstat(fd)
set_winsize(fd, data["rows"], data["cols"])
except (OSError, IOError):
# 文件描述符无效,清理资源
disconnect()
session['terminal_config'] = TERM_INIT_CONFIG
return
def on_disconnect(self): def on_disconnect(self):
child_pid = session.get('terminal_config', {}).get('child_pid') terminal_config = session.get('terminal_config', {})
child_pid = terminal_config.get('child_pid')
fd = terminal_config.get('fd')
# 先关闭文件描述符,避免其他操作尝试使用无效的文件描述符
if fd:
try:
os.close(fd)
except Exception:
pass
if child_pid: if child_pid:
try: try:
child_process = psutil.Process(child_pid) child_process = psutil.Process(child_pid)
@@ -196,21 +239,14 @@ class VSCLikeNameSpace(Namespace):
pass pass
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
child_process.kill()
try: try:
child_process.kill()
child_process.wait(timeout=1) child_process.wait(timeout=1)
except Exception: except Exception:
pass pass
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}')
finally:
# Close the file descriptor if it's open # Reset session config
fd = session.get('terminal_config', {}).get('fd') session['terminal_config'] = TERM_INIT_CONFIG
if fd:
try:
os.close(fd)
except Exception:
pass
# Reset session config
session['terminal_config'] = TERM_INIT_CONFIG
current_app.logger.debug('Client disconnected') current_app.logger.debug('Client disconnected')

View File

@@ -1,12 +1,32 @@
from app import create_app from app import create_app
from app.extensions import socketio from app.extensions import socketio
import logging
# 配置日志减少eventlet的调试日志
logging.getLogger('eventlet').setLevel(logging.ERROR)
logging.getLogger('socketio').setLevel(logging.ERROR)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
socketio.run( try:
app, socketio.run(
host="0.0.0.0", app,
port=5200, host="0.0.0.0",
debug=False, # 开发期打开 port=5200,
allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示 debug=False, # 开发期打开
allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示
log_output=False # 禁用详细输出
) )
except KeyboardInterrupt:
print("Server stopped by user")
except Exception as e:
# 捕获所有异常,包括 eventlet 内部的 IOClosed 错误
if "IOClosed" in str(type(e).__name__) or "Operation on closed file" in str(e):
# 优雅处理 eventlet 关闭文件的错误
print("Eventlet IOClosed error handled gracefully")
else:
# 其他异常仍然打印
import traceback
traceback.print_exc()