INTEGRITY Документация

Терминалы

A терминал это интерактивный PTY в текущем контейнере песочницы. Используйте его для полнодуплексного ввода-вывода терминала: оболочка в браузере, изменение размера, прерывание и переподключение.

Выполнение команд использует exec и process handle. Terminals представляют собой отдельный тип ресурса. Справочник API: Terminals API.

Процессы и терминалы

Процесс (exec) Терминал
Роль Управляемый процесс argv Интерактивный PTY
Ввод argv при запуске (и всё, что программа считывает самостоятельно) Ввод PTY через write() или браузер connect()
Вывод logs(), output(), ожидает output(), снимок, waitForExit()
Остановка kill(signal?) interrupt() / terminate()
Поиск getProcess / listProcesses getTerminal / listTerminals

Оба типа ресурсов существуют только в текущем контейнере для данного ID песочницы. Методы поиска не запускают контейнер. См. Жизненный цикл песочницы и Сколько живёт процесс.

Создайте терминал

const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	cols: 120,
	rows: 40,
});

console.log(terminal.id);
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	cols: 120,
	rows: 40,
});

console.log(terminal.id);

Из Worker можно записывать данные в PTY, изменять его размер, передавать вывод потоком или завершать его:

await terminal.write(new TextEncoder().encode("uname -a\n"));
await terminal.resize(100, 30);
await terminal.terminate();
await terminal.write(new TextEncoder().encode("uname -a\n"));
await terminal.resize(100, 30);
await terminal.terminate();

Lifetime

Сохраните terminal.id чтобы возобновить работу с тем же PTY, пока этот контейнер ещё запущен.

Подключение к браузеру

  1. Создайте терминал и сохраните terminal.id с идентификатором песочницы.
  2. При каждом обновлении до WebSocket разрешайте терминал с помощью getTerminal, затем верните terminal.connect(request).
  3. В браузере используйте @cloudflare/sandbox/xterm с terminalId.

Worker

import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade")?.toLowerCase() === "websocket"
		) {
			const sandboxId = url.searchParams.get("sandboxId");
			const terminalId = url.searchParams.get("terminalId");
			if (!sandboxId || !terminalId) {
				return new Response("sandboxId and terminalId are required", {
					status: 400,
				});
			}

			const sandbox = getSandbox(env.Sandbox, sandboxId);
			const terminal = await sandbox.getTerminal(terminalId);
			if (!terminal) {
				return new Response("Terminal not found", { status: 404 });
			}

			return terminal.connect(request, {
				cursor: url.searchParams.get("cursor") ?? undefined,
			});
		}

		return new Response("Not found", { status: 404 });
	},
};
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade")?.toLowerCase() === "websocket"
		) {
			const sandboxId = url.searchParams.get("sandboxId");
			const terminalId = url.searchParams.get("terminalId");
			if (!sandboxId || !terminalId) {
				return new Response("sandboxId and terminalId are required", {
					status: 400,
				});
			}

			const sandbox = getSandbox(env.Sandbox, sandboxId);
			const terminal = await sandbox.getTerminal(terminalId);
			if (!terminal) {
				return new Response("Terminal not found", { status: 404 });
			}

			return terminal.connect(request, {
				cursor: url.searchParams.get("cursor") ?? undefined,
			});
		}

		return new Response("Not found", { status: 404 });
	},
};

Создайте терминал из маршрута приложения, когда он нужен интерфейсу:

const sandboxId = "user-123";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const terminal = await sandbox.createTerminal({ command: ["bash"] });
return Response.json({ sandboxId, terminalId: terminal.id });
const sandboxId = "user-123";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const terminal = await sandbox.createTerminal({ command: ["bash"] });
return Response.json({ sandboxId, terminalId: terminal.id });

Браузер (xterm.js)

npm install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox@next
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
const sandboxAddon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
});

term.loadAddon(fitAddon);
term.loadAddon(sandboxAddon);
term.open(document.getElementById("terminal"));
fitAddon.fit();

// Values returned by your create-terminal route
const sandboxId = "user-123";
const terminalId = "term_...";
sandboxAddon.connect({ sandboxId, terminalId });
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
const sandboxAddon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
});

term.loadAddon(fitAddon);
term.loadAddon(sandboxAddon);
term.open(document.getElementById("terminal")!);
fitAddon.fit();

// Values returned by your create-terminal route
const sandboxId = "user-123";
const terminalId = "term_...";
sandboxAddon.connect({ sandboxId, terminalId });
Стабильный пакет Предпросмотр
sandbox.terminal(request) createTerminal + getTerminal + connect
xterm / URL sessionId terminalId (и необязательный cursor)