在 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 添加到
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 -
将 在浏览器中打开终端 中的
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。 -
在
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。有了此消息,切换前加载的页面在重连时会继续工作。 -
如果
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以外的命令,请参阅 在终端中运行其他命令。 -
如果你的类转发来自 迁移预览 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); } }
在 0.12 中,Worker 将 WebSocket 请求传给 terminal():
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 查询参数随之传递:
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 标头。否则发送该标头的访问者会在没有预览令牌的情况下到达端口:
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 }));1.0 没有 @cloudflare/sandbox/xterm 模块。在 0.12 中,页面将 SandboxAddon 加载到 xterm.js:
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,并在关闭时重连:
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 sizeshell 在 /workspace 中启动,并带有来自 ENV 的变量,tmux 会为其状态行占用一行:
pwd=/workspace NODE_ENV=test
29 100启动每秒打印一行的循环,然后重新加载页面。计数会继续。运行 exit。WebSocket 以代码 1000 和 reason Terminal closed with code 0 关闭,页面不会重连。