You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Django+Xterm.js的浏览器终端多客户端隔离问题求助

问题

基于Django开发的浏览器终端应用,使用Xterm.js负责可视化与用户输入采集,终端逻辑由伪终端(pty)和bash进程处理。单客户端使用时运行正常,但新客户端连接或在新标签页打开时,会共用同一终端实例。由于客户端数量不确定,无法提前定义多个Xterm.js终端实例(如termA、termB)。尝试在Django中实现多进程未成功,寻求可行解决方案。

核心原因

当前代码使用**全局变量fd和child_pid**存储终端实例信息,所有客户端连接都会复用这两个变量,导致共用同一个bash进程。

解决方案

为每个Socket连接(对应唯一的sid)创建独立的pty与bash进程,使用字典存储每个sid对应的进程信息,实现客户端与终端实例的一一绑定。

修改步骤

  • 替换全局变量为字典,存储每个客户端的进程信息
  • 客户端连接时,为其创建独立的pty和bash进程
  • 终端输出仅转发给对应的客户端
  • 输入、窗口调整操作仅针对当前客户端的终端实例
  • 客户端断开时,清理对应的进程与字典条目
修改后的代码

views.py

import os
from django.shortcuts import render
import socketio
import pty
import select
import subprocess
import struct
import fcntl
import termios
import signal
import eventlet    

async_mode = "eventlet"
sio = socketio.Server(async_mode=async_mode)

# 用字典存储每个客户端的会话信息:key为sid,value为{"pid": 进程ID, "fd": 文件描述符}
client_sessions = {}

def index(request):
    return render(request, "index.html")

def set_winsize(fd, row, col, xpix=0, ypix=0):
    winsize = struct.pack("HHHH", row, col, xpix, ypix)
    fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)

def read_and_forward_pty_output(sid):
    max_read_bytes = 1024 * 20
    while True:
        sio.sleep(0.01)
        # 获取当前客户端的终端信息
        session = client_sessions.get(sid)
        if not session:
            print(f"Session {sid} closed, stop forwarding")
            return
        
        fd = session["fd"]
        timeout_sec = 0
        (data_ready, _, _) = select.select([fd], [], [], timeout_sec)
        if data_ready:
            output = os.read(fd, max_read_bytes).decode()
            # 仅向当前sid的客户端发送输出
            sio.emit("pty_output", {"output": output}, room=sid)

@sio.event
def resize(sid, message):
    session = client_sessions.get(sid)
    if session:
        set_winsize(session["fd"], message["rows"], message["cols"])

@sio.event
def pty_input(sid, message):
    session = client_sessions.get(sid)
    if session:
        os.write(session["fd"], message["input"].encode())

@sio.event
def disconnect_request(sid):
    sio.disconnect(sid)

@sio.event
def connect(sid, environ):
    # 为每个新连接创建独立的pty和bash进程
    (child_pid, fd) = pty.fork()

    if child_pid == 0:
        subprocess.run("clear")
        subprocess.run('bash')
    else:
        # 存储当前客户端的进程信息
        client_sessions[sid] = {"pid": child_pid, "fd": fd}
        # 启动针对当前客户端的输出转发任务
        sio.start_background_task(target=read_and_forward_pty_output, sid=sid)

@sio.event
def disconnect(sid):
    session = client_sessions.pop(sid, None)
    if session:
        child_pid = session["pid"]
        try:
            os.kill(child_pid, signal.SIGKILL)
            os.waitpid(child_pid, 0)
            print(f"Client {sid} disconnected, terminal process killed")
        except OSError:
            # 进程可能已提前结束,忽略错误
            pass

index.html

<html>

<head>
    {% load static %}
    <link rel="stylesheet" href="https://unpkg.com/xterm@5.2.1/css/xterm.css" />

    <script src="https://unpkg.com/xterm@5.2.1/lib/xterm.js"></script>
    <script src="https://unpkg.com/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.js"></script>
</head>

<body>

    <div class="main" style="margin-left: 5%; position:sticky;">

        <div style="background: white; padding-bottom: 5px; margin-top:1%;">
            <span>Status: <span style="font-size: small; background-color: #d9ff00;"
                    id="status">connecting...</span></span>
            <button id="button" type="button" onclick="myFunction()" style="padding:0.2%;">Connect</button>
        </div>

        <div style=" width: 98%; height:50%;" id="terminal"></div>

    </div>

    <script>

        var socket = io.connect({ transports: ["websocket", "polling"], autoConnect: false });

        const status = document.getElementById("status")
        const button = document.getElementById("button")
        const fit = new FitAddon.FitAddon();

        // Clear sessionStorage when the page is reloaded or closed
        window.addEventListener('beforeunload', () => {
            sessionStorage.clear();
        });

        var term = new Terminal({
            cursorBlink: true,
        });

        term.loadAddon(fit);
        term.open(document.getElementById('terminal'));
        fit.fit();


        term.onKey(e => {
            socket.emit("pty_input", { "input": e.key });
        })

        socket.on("pty_output", function (output) {
            term.write(output["output"]);
        })

        socket.on("connect", () => {
            status.innerHTML = '<span style="background-color: lightgreen;">connected</span>'
            resize()
            button.innerHTML = 'Disconnect'
        })

        socket.on("disconnect", () => {
            status.innerHTML = '<span style="background-color: #ff8383;">disconnected</span>'
            button.innerHTML = 'Connect'
        })

        function myFunction() {
            if (button.innerHTML == 'Connect') {
                console.log("conn req")
                socket.connect();
            }

            else if (button.innerHTML == "Disconnect") {
                socket.emit("disconnect_request")
            }
        }

        function resize() {
            console.log("resized")
            fit.fit()
            socket.emit("resize", { "cols": term.cols, "rows": term.rows })
        }
        window.onresize = resize
        window.onload = resize

    </script>
</body>

</html>
关键说明
  • 使用client_sessions字典绑定每个sid与对应的终端进程,确保客户端隔离
  • read_and_forward_pty_output函数增加sid参数,仅向该客户端发送终端输出
  • 断开连接时从字典中移除对应条目并销毁进程,避免资源泄漏
  • 前端无需大幅修改,每个页面会创建独立Socket连接,自动绑定到专属终端实例

内容的提问来源于stack exchange,提问作者ivenoidea

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 03:04:54