terminal
This commit is contained in:
@@ -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)
|
||||
@@ -21,6 +21,11 @@
|
||||
}
|
||||
|
||||
|
||||
#terminal {
|
||||
flex-grow: 1;
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
41
code-server-like/static/js/terminal.js
Normal file
41
code-server-like/static/js/terminal.js
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -7,12 +7,13 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.15.0/dist/xterm.css">
|
||||
|
||||
|
||||
<style>
|
||||
body { display: flex; height: 100vh; margin: 0; }
|
||||
#fileTree { width: 250px; border-right: 1px solid #ddd; padding: 10px; overflow-y: auto; }
|
||||
#editorContainer { flex-grow: 1; }
|
||||
#terminal { height: 200px; border-top: 1px solid #ddd; }
|
||||
#searchSidebar { border-left: 1px solid #ddd; display: none; overflow-y: auto; }
|
||||
#topbar { padding: 5px; border-bottom: 1px solid #ddd; background-color: #f1f1f1; }
|
||||
|
||||
@@ -53,6 +54,7 @@ body { display: flex; height: 100vh; margin: 0; }
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.js"></script>
|
||||
<script src="/vsc-like/static/js/file.js"></script>
|
||||
<script src="/vsc-like/static/js/terminal.js"></script>
|
||||
<script>
|
||||
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user