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 @@
+
+