From 628e4c7805cd7eb08a92f8658266202cd48cf618 Mon Sep 17 00:00:00 2001 From: CakeCN Date: Mon, 15 Sep 2025 19:26:11 +0800 Subject: [PATCH] terminal --- .../app/sockets/terminal_service.py | 124 +++++++++++++++--- code-server-like/static/css/index.css | 5 + code-server-like/static/js/terminal.js | 41 ++++++ code-server-like/templates/index.html | 4 +- 4 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 code-server-like/static/js/terminal.js diff --git a/code-server-like/app/sockets/terminal_service.py b/code-server-like/app/sockets/terminal_service.py index b6ab74f..494ea3a 100644 --- a/code-server-like/app/sockets/terminal_service.py +++ b/code-server-like/app/sockets/terminal_service.py @@ -1,5 +1,7 @@ import os +import select import subprocess +import threading from flask import Blueprint, request, current_app, session from flask_socketio import SocketIO, emit, join_room from threading import Thread @@ -29,36 +31,124 @@ def start_terminal_process(socket_id): if process.poll() is not None: break + +def start_terminal_process(socket_id): + """ + 监听多个子进程的输出,并通过 WebSocket 发给前端 + 使用 select 进行非阻塞的 I/O 操作,并实现动态流量控制 + """ + process = terminals[socket_id] + + # 获取子进程的 stdout 和 stderr + stdout_fd = process.stdout + stderr_fd = process.stderr + + # 将 stdout 和 stderr 添加到 select 监听的文件描述符列表中 + fds = [stdout_fd, stderr_fd] + + # 初始等待时间 + timeout = 0.1 + max_timeout = 2 # 设置最大超时时间为 2 秒 + + while True: + # 使用 select 监听多个文件描述符,等待数据准备好 + readable, _, _ = select.select(fds, [], [], timeout) + + if readable: + # 有数据可读时,重置超时时间为 0.1 秒 + timeout = 0.1 + + for fd in readable: + # 读取进程的标准输出或错误输出 + output = fd.readline() + if output: + emit('terminal_output', {'data': output}, room=socket_id) + else: + # 如果没有数据可读,则逐渐增加超时时间 + timeout = min(timeout * 2, max_timeout) + + # 检查进程是否仍在运行,如果不在运行则退出循环 + if process.poll() is not None: + break + @socketio.on('connect', namespace='/vsc-like/terminal') def connect_terminal(): + """ + 处理 WebSocket 连接 + - 验证会话 + - 使用指定的用户启动 bash 终端 + - 启动监听子进程输出的线程 + """ socket_id = session.get('uuid') + user_id = session.get('user_id') # 从 session 获取用户 ID path = session.get('path') - if not path: - emit('error', {'message': 'Authentication failed, no path found for this session'}) + + if not path or not user_id: + emit('error', {'message': 'Authentication failed, no path or user found for this session'}) return False # 拒绝连接 - terminals[socket_id] = subprocess.Popen( - ['bash'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) - emit('connected', {'message': 'Terminal connected'}) - join_room(path, namespace='/vsc-like/terminal') - # 启动监听终端输出的线程 - thread = Thread(target=start_terminal_process, args=(socket_id,)) - thread.daemon = True - thread.start() + + # 使用 sudo 启动终端进程,以指定的 user_id 用户身份执行 + try: + process = subprocess.Popen( + ['sudo', '-u', user_id, 'bash'], # 使用指定的 user_id 启动 bash + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + terminals[socket_id] = process + + emit('connected', {'message': 'Terminal connected'}) + join_room(path, namespace='/vsc-like/terminal') + + # 启动监听终端输出的线程 + thread = Thread(target=start_terminal_process, args=(socket_id,)) + thread.daemon = True + thread.start() + + except Exception as e: + emit('error', {'message': f'Error starting terminal process: {str(e)}'}) + return False + +terminal_locks = {} # 存储终端的锁,用于线程同步 @socketio.on('send_input', namespace='/vsc-like/terminal') def send_input(data): socket_id = session.get('uuid') - path = session.get('path') input_data = data.get('input') + + # 检查当前 socket_id 是否有对应的终端 if socket_id in terminals: - terminals[socket_id].stdin.write(input_data + '\n') - terminals[socket_id].stdin.flush() + # 获取该终端的锁进行线程同步 + 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 + '\n') + terminals[socket_id].stdin.flush() + emit('terminal_output', {'data': f'Command sent: {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) + @socketio.on('disconnect', namespace='/vsc-like/terminal') def disconnect_terminal(): socket_id = session.get('uuid') - path = session.get('path') + + # 检查是否有终端进程需要终止 if socket_id in terminals: - terminals[socket_id].terminate() - del terminals[socket_id] + 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) \ No newline at end of file diff --git a/code-server-like/static/css/index.css b/code-server-like/static/css/index.css index 4411d97..17bec68 100644 --- a/code-server-like/static/css/index.css +++ b/code-server-like/static/css/index.css @@ -21,6 +21,11 @@ } + #terminal { + flex-grow: 1; + border-top: 1px solid #ddd; + background-color: black; +} diff --git a/code-server-like/static/js/terminal.js b/code-server-like/static/js/terminal.js new file mode 100644 index 0000000..10a2063 --- /dev/null +++ b/code-server-like/static/js/terminal.js @@ -0,0 +1,41 @@ +const socket = io.connect('https://hsamooc.cn/vsc-like/terminal'); + let term; + + document.addEventListener('DOMContentLoaded', () => { + // 创建一个新的 xterm 实例 + term = new Terminal({ + cursorBlink: true, + cols: 80, + rows: 24, + }); + + // 打开终端到 #terminal 元素 + term.open(document.getElementById('terminal')); + + // 启动连接并监听数据 + socket.on('connected', function (data) { + term.write('Terminal connected\n'); + }); + + // 监听后端返回的终端输出并显示 + socket.on('terminal_output', function (data) { + term.write(data.data); // 输出来自后端的数据 + }); + + // 监听用户在终端中输入的命令 + term.onData(function (data) { + socket.emit('send_input', { input: data }); // 将用户输入通过 WebSocket 发送给后端 + }); + + // 用户输入命令时,将其发送给后端 + socket.emit('send_input', { input: 'start' }); + + // 如果终端被断开,显示消息 + socket.on('error', function (error) { + term.write(`\nError: ${error.message}`); + }); + + socket.on('disconnect', function () { + term.write('\nDisconnected from terminal'); + }); + }); \ No newline at end of file diff --git a/code-server-like/templates/index.html b/code-server-like/templates/index.html index a0824fa..29b6245 100644 --- a/code-server-like/templates/index.html +++ b/code-server-like/templates/index.html @@ -7,12 +7,13 @@ + +