跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

迁移浏览器终端

最后更新 查看 MarkdownAgent 设置

在 Sandbox SDK 0.12 中,terminal() 将 WebSocket 附加到容器为每个会话保留的 shell,SandboxAddon 将浏览器重新连接到它。在 1.0 中,你的 Durable Object 用 exec(argv, { pty }) 将每个 WebSocket 附加到 tmux ↗ 会话。tmux 在连接之间保持 shell 运行。

开始之前

你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter、ensureRunning() 方法和 ENV 常量。

本页替换 terminal()、session.terminal()、proxyTerminal() 以及来自 @cloudflare/sandbox/xterm 的 SandboxAddon。它不涵盖命令会话。那些请参阅 替换会话。

将终端附加到 tmux

  1. 将 tmux 添加到 Dockerfile:

    Dockerfiledockerfile
    RUN apt-get update \
        && apt-get install -y --no-install-recommends \
            ca-certificates git python3 tmux \
        && rm -rf /var/lib/apt/lists/*
    
    # Scroll back with the mouse wheel, and keep more history.
    RUN printf 'set -g mouse on\nset -g history-limit 50000\n' \
        > /etc/tmux.conf
  2. 将 在浏览器中打开终端 中的 fetch() handler 添加到你的类。将其 container 常量、start() 调用和 setInactivityTimeout() 调用替换为 ensureRunning(),并用 0.12 使用的目录和变量启动 shell:

    src/index.tsts
    await this.ensureRunning();
    
    const abort = new AbortController();
    const tmux = await this.container.exec(
        ["tmux", "new-session", "-A", "-s", session],
        {
            pty: {
                cols: Number(url.searchParams.get("cols")) || 80,
                rows: Number(url.searchParams.get("rows")) || 24,
            },
            env: { ...ENV, TERM: "xterm-256color" },
            cwd: "/workspace",
            signal: abort.signal,
        },
    );

    0.12 在 /workspace 中启动每个 shell,并带有来自 envVars 和镜像 ENV 行的变量。exec() 只从其中继承 PATH,因此请将 shell 需要的任何其他镜像变量添加到 ENV。

  3. 在 accept() 之后立即发送 0.12 客户端等待的消息:

    src/index.tsts
    server.binaryType = "arraybuffer";
    server.accept();
    
    // 0.12 clients wait for this message before they send their size.
    server.send(JSON.stringify({ type: "ready" }));

    SandboxAddon 和其他为 0.12 消息编写的客户端会保持 connecting 状态,直到 {"type":"ready"} 到达。然后它们发送窗口大小。handler 已经从同样带有 "type": "resize" 的 0.12 调整大小消息中读取 cols 和 rows。有了此消息,切换前加载的页面在重连时会继续工作。

  4. 如果 createSession() 给会话提供了自己的工作目录和变量,请在页面打开该会话时将它们传给 tmux:

    src/index.tsts
    // The working directory and variables of each 0.12 session
    // that browsers open by name.
    type SessionOptions = { cwd: string; env: Record<string, string> };
    
    const SESSIONS: Record<string, SessionOptions> = {
        build: { cwd: "/workspace/app", env: { CI: "1" } },
    };

    在 handler 中,从会话名称构建 tmux 命令,并将 argv 传给 exec() 以替换该数组:

    src/index.tsts
    const argv = ["tmux", "new-session", "-A", "-s", session];
    const options = SESSIONS[session];
    
    if (options) {
        argv.push("-c", options.cwd);
    
        for (const [name, value] of Object.entries(options.env)) {
            argv.push("-e", `${name}=${value}`);
        }
    }

    tmux 在创建会话时应用 -c 和 -e。没有 ?session= 的请求会打开会话 main,它替换 0.12 默认会话。

    handler 接受小写字母、数字和连字符的会话名称,tmux 不接受 . 或 :。请重命名使用其他字符的 0.12 会话。

    要像 0.12 shell 选项那样运行 bash 以外的命令,请参阅 在终端中运行其他命令。

  5. 如果你的类转发来自 迁移预览 URL 的预览 URL,它已经有 fetch() handler。将该 handler 重命名为 forwardToPort(),将终端 handler 重命名为 openTerminal(),使两者都为 private,并添加选择其中一个的 fetch() handler:

    src/index.tsts
    export class MySandbox extends DurableObject<Env> {
        // ...
    
        async fetch(request: Request): Promise<Response> {
            // Only the preview route of the Worker sets this header.
            if (request.headers.has("X-Sandbox-Port")) {
                return this.forwardToPort(request);
            }
    
            return this.openTerminal(request);
        }
    }

转发 WebSocket

在 0.12 中,Worker 将 WebSocket 请求传给 terminal():

src/index.ts (0.12)ts
const sandbox = getSandbox(env.MY_SANDBOX, id);
const sessionId = url.searchParams.get("session");

if (sessionId) {
    const session = await sandbox.getSession(sessionId);
    return session.terminal(request);
}

return sandbox.terminal(request, { cols: 80, rows: 24 });

在 1.0 中,Worker 将请求转发到 Durable Object,session 查询参数随之传递:

src/index.ts (1.0)ts
return env.MY_SANDBOX.getByName(id).fetch(request);

传入 0.12 使用的名称。除非你传入 normalizeId: true,否则 0.12 会保留名称的大小写。如果传入了该选项,请调用 id.toLowerCase(),如替换 Sandbox 类所示。

将 proxyTerminal(stub, sessionId, request) 替换为相同的 fetch() 调用,并将会话放在 session 查询参数中。handler 从 cols 和 rows 查询参数读取大小,没有它们时使用 80×24。没有 WebSocket 升级的请求会得到 HTTP 426,而 0.12 会抛出错误。

如果你的类也转发预览 URL,请在转发请求之前移除 X-Sandbox-Port 标头。否则发送该标头的访问者会在没有预览令牌的情况下到达端口:

src/index.ts (1.0)ts
const headers = new Headers(request.headers);
headers.delete("X-Sandbox-Port");

const sandbox = env.MY_SANDBOX.getByName(id);
return sandbox.fetch(new Request(request, { headers }));

替换 SandboxAddon

1.0 没有 @cloudflare/sandbox/xterm 模块。在 0.12 中,页面将 SandboxAddon 加载到 xterm.js:

terminal.ts (0.12)ts
import { SandboxAddon } from "@cloudflare/sandbox/xterm";

const addon = new SandboxAddon({
    getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
        const params = new URLSearchParams({ id: sandboxId });
        if (sessionId) params.set("session", sessionId);
        return `${origin}/ws/terminal?${params}`;
    },
    onStateChange: (state) => console.log(state),
});

terminal.loadAddon(addon);
addon.connect({ sandboxId: "my-sandbox", sessionId: "build" });

在 1.0 中,页面自己打开 WebSocket,并在关闭时重连:

terminal.ts (1.0)ts
const encoder = new TextEncoder();
let socket: WebSocket;
let attempts = 0;

const send = (data: string | Uint8Array) => {
    if (socket.readyState === WebSocket.OPEN) socket.send(data);
};

const connect = () => {
    const url = new URL("/ws/terminal", location.href);
    url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
    url.searchParams.set("id", "my-sandbox");
    url.searchParams.set("session", "build");
    url.searchParams.set("cols", String(terminal.cols));
    url.searchParams.set("rows", String(terminal.rows));

    socket = new WebSocket(url);
    socket.binaryType = "arraybuffer";

    socket.addEventListener("open", () => {
        attempts = 0;
        // tmux redraws the whole screen when a client attaches.
        terminal.reset();
    });
    socket.addEventListener("message", (event) => {
        // Skip the ready message.
        if (typeof event.data === "string") return;
        terminal.write(new Uint8Array(event.data));
    });
    socket.addEventListener("close", (event) => {
        // Code 1000 means the shell exited or the person detached.
        if (event.code === 1000 || attempts >= 10) return;
        const delay = Math.min(1000 * 2 ** attempts++, 30_000);
        setTimeout(connect, delay);
    });
};

connect();
terminal.onData((data) => send(encoder.encode(data)));
terminal.onResize(({ cols, rows }) => {
    send(JSON.stringify({ cols, rows }));
});

addon 的状态映射到套接字事件。connecting 持续到 open 事件,connected 随之开始。disconnected 跟随页面不重试的 close 事件。

与 addon 一样,页面在 1 秒后重试,延迟加倍,并在 10 次尝试后停止。升级被 HTTP 错误取代时,会作为代码 1006 的 close 事件到达。

终端上的变化

重连的页面显示 tmux 重绘的屏幕,而 0.12 会回放最多 256 KiB 的更早输出。更早的输出留在 tmux 历史中,因此人们可以用鼠标滚轮回滚,并按 q 离开历史。

当 shell 退出时,套接字以代码 1000 关闭,reason 携带退出码,例如 Terminal closed with code 0。下一次连接会启动新 shell。0.12 会保留已结束的终端,因此后续连接会显示其旧输出,并且在容器重启前什么都不运行。

用 CTRL + B 然后 D 分离也会以代码 1000 关闭套接字,会话会继续运行。

打开同一会话的浏览器共享一个屏幕,与 0.12 一样。

切换时已打开的终端

切换会结束 0.12 shell 及其输出。已打开的终端会停止接收数据且没有 close 事件,因此 SandboxAddon 不会重连。请让人们在部署后重新加载页面,或添加 规划迁移 中的读取超时。重新加载后,Durable Object 会创建新的 tmux 会话。

检查终端

部署切换后,为 sandbox 打开终端,并将窗口设为 100 列 × 30 行。在浏览器开发者工具中,WebSocket 上的第一条消息是 {"type":"ready"}。在终端中运行此命令:

echo pwd=$(pwd) NODE_ENV=$NODE_ENV; stty size

shell 在 /workspace 中启动,并带有来自 ENV 的变量,tmux 会为其状态行占用一行:

pwd=/workspace NODE_ENV=test
29 100

启动每秒打印一行的循环,然后重新加载页面。计数会继续。运行 exit。WebSocket 以代码 1000 和 reason Terminal closed with code 0 关闭,页面不会重连。

相关资源

这篇文档对您有帮助吗?