Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 53 additions & 33 deletions crates/flock-tools/src/sandbox_core/e2b/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,22 @@ which Xvfb || echo "Xvfb missing"
which fluxbox || echo "fluxbox missing"
which x11vnc || echo "x11vnc missing"
which websockify || echo "websockify missing"
which chromium || echo "chromium (system) missing"
which google-chrome || echo "google-chrome missing"
which google-chrome-stable || echo "google-chrome-stable missing"

echo "=== Debug: Testing Chrome Execution & Shared Libraries ==="
google-chrome-stable --version 2>&1 || echo "Chrome version check failed"
ldd /usr/bin/google-chrome-stable | grep "not found" || echo "All shared library dependencies satisfied"
echo "=========================================="

export DISPLAY=:0
rm -f /tmp/.X0-lock
setsid Xvfb :0 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 &
setsid Xvfb :0 -screen 0 1280x800x24 -ac +extension GLX +render -noreset >/tmp/xvfb.log 2>&1 &
sleep 1
setsid fluxbox >/tmp/fluxbox.log 2>&1 &
sleep 1
setsid x11vnc -display :0 -forever -shared -nopw -rfbport 5900 >/tmp/x11vnc.log 2>&1 &
sleep 2
setsid x11vnc -display :0 -forever -shared -nopw -rfbport 5900 -noxrecord -noxfixes -noxdamage >/tmp/x11vnc.log 2>&1 &
sleep 1
setsid websockify --web /usr/share/novnc 0.0.0.0:6080 127.0.0.1:5900 >/tmp/websockify.log 2>&1 &
sleep 1
Expand All @@ -51,27 +58,44 @@ sleep 1
std::fs::write(&start_script_path, lf_content)
.context("Failed to write start_command.sh")?;

// 1. 写入 Dockerfile
let dockerfile_content = r#"FROM e2bdev/desktop:latest
USER root
# 复制启动脚本并赋予执行权限
COPY start_command.sh /start_command.sh
RUN chmod +x /start_command.sh

# 安装缺少的 VNC 窗口管理器和 noVNC 代理组件
RUN apt-get update && apt-get install -y fluxbox websockify novnc

# 直接使用已有的 python3 和 pip 安装 Playwright 及其浏览器依赖
RUN pip install playwright && \
playwright install chromium && \
playwright install-deps chromium

USER user
"#;
// 写入 Dockerfile
// 策略:使用官方 deb 包安装 Google Chrome Stable(完整 GUI,原生 X11 支持,VNC 可见)
// 绕过 Debian 下 apt 直接装 chromium 不存在的问题,同时也避免了 playwright install 软连接失效问题。
let dockerfile_content = "FROM e2bdev/desktop:latest\n\
USER root\n\
\n\
# 复制启动脚本\n\
COPY start_command.sh /start_command.sh\n\
RUN chmod +x /start_command.sh\n\
\n\
# 安装 VNC 桌面组件\n\
RUN apt-get update && apt-get install -y --no-install-recommends fluxbox websockify novnc xterm wget gnupg && rm -rf /var/lib/apt/lists/*\n\
\n\
# CJK 字体(可选)\n\
RUN apt-get update && apt-get install -y --no-install-recommends fonts-wqy-zenhei fonts-wqy-microhei 2>/dev/null; apt-get install -y --no-install-recommends fonts-noto-cjk 2>/dev/null; rm -rf /var/lib/apt/lists/*; true\n\
\n\
# 安装 Google Chrome 稳定版(含完整 GUI,支持 X11,VNC 中可见)\n\
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \\\n\
&& echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list \\\n\
&& apt-get update \\\n\
&& apt-get install -y --no-install-recommends google-chrome-stable \\\n\
&& rm -rf /var/lib/apt/lists/*\n\
\n\
# 创建 chromium 软链接,使得支持 chromium 命令的工具能自动调用它\n\
RUN ln -sf /usr/bin/google-chrome-stable /usr/local/bin/chromium \\\n\
&& ln -sf /usr/bin/google-chrome-stable /usr/local/bin/google-chrome\n\
\n\
# 安装 Playwright Python 库(仅库本身,不下载 playwright 浏览器)\n\
# 运行时由 PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH 指向 google-chrome-stable\n\
RUN pip3 install playwright\n\
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable\n\
\n\
USER user\n\
";
std::fs::write(&dockerfile_path, dockerfile_content)
.context("Failed to write Dockerfile")?;

on_log("环境准备完毕,开始调用 E2B CLI 构建镜像 (E2B 2.0 在云端构建,不需要本地安装 Docker)...\n".to_string());
on_log("Environment setup complete. Invoking E2B CLI to build the image (E2B 2.0 builds in the cloud; no local Docker installation is required)...\n".to_string());

// 3. 执行 npx @e2b/cli template create
// 在 Windows 下需要调用 npx.cmd
Expand All @@ -88,6 +112,10 @@ USER user
.arg("/start_command.sh")
.arg("--ready-cmd")
.arg("python3 -c \"import socket; s = socket.socket(); s.connect(('127.0.0.1', 6080))\"")
.arg("--cpu-count")
.arg("2")
.arg("--memory-mb")
.arg("2048")
.arg(name)
.env("E2B_API_KEY", api_key)
.current_dir(&builder_dir)
Expand Down Expand Up @@ -137,23 +165,15 @@ USER user
anyhow::bail!("E2B template build failed with exit code: {}.\nError details:\n{}", status, full_output);
}

on_log("构建成功!正在解析 Template ID...\n".to_string());
on_log("Build succeeded! Parsing Template ID...\n".to_string());

// 解析出 template ID
let re = regex::Regex::new(r"(?i)\b(?:id|template_id)[:\s]+([a-z0-9]{20})\b").unwrap();
let re = regex::Regex::new(r"(?i)\b(?:id|template_id)[\s:]+([a-z0-9]{20})\b").unwrap();
if let Some(cap) = re.captures(&full_output) {
let id = cap.get(1).unwrap().as_str().to_string();
on_log(format!("Template ID successfully parsed: {}\n", id));
return Ok(id);
}

// 回退解析:寻找任何非 "dockerfile" 的 20 位字母数字组合作为 ID
let re_fallback = regex::Regex::new(r"\b([a-z0-9]{20})\b").unwrap();
for cap in re_fallback.captures_iter(&full_output) {
let matched = cap.get(1).unwrap().as_str();
if matched != "dockerfile" && matched != "template" {
return Ok(matched.to_string());
}
}

anyhow::bail!("构建成功,但未能从 E2B CLI 输出日志中提取出生成的 template_id")
anyhow::bail!("Template build succeeded, but failed to extract the generated templateID from E2B CLI logs")
}
79 changes: 40 additions & 39 deletions crates/flock-tools/src/sandbox_core/e2b/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,18 @@ pub async fn execute_command(

let body_str = serde_json::to_string(&payload)?;

// 拼接 5 字节的 Connect 协议信封头部 (1 字节 flag + 4 字节大端序长度)
let mut req_body = Vec::new();
req_body.push(0x00);
req_body.extend_from_slice(&(body_str.len() as u32).to_be_bytes());
req_body.extend_from_slice(body_str.as_bytes());

let resp = client.post(&url)
.header("X-API-Key", api_key)
.header("Connect-Protocol-Version", "1")
.header("Content-Type", "application/connect+json")
.header(reqwest::header::USER_AGENT, "flock-agent")
.body(body_str)
.body(req_body)
.send()
.await?;

Expand All @@ -56,53 +62,48 @@ pub async fn execute_command(
let mut stderr_accum = String::new();
let mut exit_code = 0;

use futures::StreamExt;
let mut body = resp.bytes_stream();
let mut buffer = Vec::new();

while let Some(chunk_res) = body.next().await {
let chunk = chunk_res?;
buffer.extend_from_slice(&chunk);

while buffer.len() >= 5 {
let flags = buffer[0];
let length = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize;

if buffer.len() >= 5 + length {
let payload_bytes = &buffer[5..5 + length];

if flags == 0x00 {
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(payload_bytes) {
if let Some(event) = val.get("event") {
if let Some(data) = event.get("data") {
if let Some(stdout_b64) = data.get("stdout").and_then(|v| v.as_str()) {
if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stdout_b64) {
if let Ok(s) = String::from_utf8(decoded) {
stdout_accum.push_str(&s);
}
let response_bytes = resp.bytes().await?;
let mut buffer = response_bytes.to_vec();
let mut idx = 0;

while idx + 5 <= buffer.len() {
let flags = buffer[idx];
let length = u32::from_be_bytes([buffer[idx+1], buffer[idx+2], buffer[idx+3], buffer[idx+4]]) as usize;

if idx + 5 + length <= buffer.len() {
let payload_bytes = &buffer[idx+5..idx+5+length];

if flags == 0x00 {
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(payload_bytes) {
if let Some(event) = val.get("event") {
if let Some(data) = event.get("data") {
if let Some(stdout_b64) = data.get("stdout").and_then(|v| v.as_str()) {
if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stdout_b64) {
if let Ok(s) = String::from_utf8(decoded) {
stdout_accum.push_str(&s);
}
}
if let Some(stderr_b64) = data.get("stderr").and_then(|v| v.as_str()) {
if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stderr_b64) {
if let Ok(s) = String::from_utf8(decoded) {
stderr_accum.push_str(&s);
}
}
if let Some(stderr_b64) = data.get("stderr").and_then(|v| v.as_str()) {
if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stderr_b64) {
if let Ok(s) = String::from_utf8(decoded) {
stderr_accum.push_str(&s);
}
}
}
if let Some(end) = event.get("end") {
if let Some(code) = end.get("exitCode").or_else(|| end.get("exit_code")).and_then(|v| v.as_i64()) {
exit_code = code as i32;
}
}
if let Some(end) = event.get("end") {
if let Some(code) = end.get("exitCode").or_else(|| end.get("exit_code")).and_then(|v| v.as_i64()) {
exit_code = code as i32;
}
}
}
}

buffer.drain(0..5 + length);
} else {
break;
}

idx += 5 + length;
} else {
break;
}
}

Expand All @@ -121,7 +122,7 @@ pub async fn execute_command(
/// 直接返回固定格式 URL,不检查端口,不拉起任何进程。
pub fn get_vnc_url(sandbox_id: &str) -> String {
format!(
"https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true",
"https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&path=websockify",
sandbox_id
)
}
5 changes: 4 additions & 1 deletion crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
pub struct E2BCreateSandboxRequest {
#[serde(rename = "templateID")]
pub template_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -46,12 +48,13 @@ pub async fn create_sandbox(cfg: &SandboxConfig) -> anyhow::Result<String> {
.unwrap_or("")
.trim();
if template_id.is_empty() {
template_id = "jj17rl441iqdc6ea5mkb"; // 使用您发布的公共模版 ID 作为默认兜底
template_id = "aa5z6s0zm7qv5g3oay5v"; // 使用您发布的公共模版 ID 作为默认兜底
}

let client = reqwest::Client::new();
let payload = E2BCreateSandboxRequest {
template_id: template_id.to_string(),
timeout: Some(300),
};

crate::emit_info(&flock_core::tr(
Expand Down
25 changes: 20 additions & 5 deletions crates/flock-tools/src/sandbox_core/vnc_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,41 @@ pub async fn ensure_vnc_running_in_sandbox(db: &DbManager, sandbox_id: &str) ->
let check_cmd = format!("python3 -c \"import socket; s = socket.socket(); s.connect(('127.0.0.1', {}))\"", WEBSOCKIFY_PORT);
let (_, exit_code) = execute_command_in_sandbox(db, sandbox_id, &check_cmd).await.unwrap_or(("-1".to_string(), -1));
if exit_code == 0 {
crate::emit_info(&flock_core::tr("检测到 VNC 桌面服务已经在运行。", "Detected VNC desktop service is already running."));
// websockify 在跑,额外检查 fluxbox 是否存活(防止桌面黑屏)
let fluxbox_check = "pgrep -x fluxbox >/dev/null 2>&1 && echo ok || echo missing";
let (fluxbox_out, _) = execute_command_in_sandbox(db, sandbox_id, fluxbox_check)
.await
.unwrap_or(("missing".to_string(), 1));
if fluxbox_out.trim() == "ok" {
crate::emit_info(&flock_core::tr("检测到 VNC 桌面服务已经在运行。", "Detected VNC desktop service is already running."));
return Ok(());
}
// fluxbox 未运行,只补启 fluxbox
crate::emit_info(&flock_core::tr("VNC 端口在线但桌面管理器未运行,正在补启 fluxbox...", "VNC port active but window manager not running, restarting fluxbox..."));
let restart_fluxbox = format!(
"setsid nohup env DISPLAY={display} fluxbox >/tmp/fluxbox.log 2>&1 & sleep 2",
display = DISPLAY_ID
);
let _ = execute_command_in_sandbox(db, sandbox_id, &restart_fluxbox).await;
return Ok(());
}

crate::emit_info(&flock_core::tr("检测到 VNC 服务未运行,手动拉起 Xvfb, VNC, noVNC...", "Detected VNC service not running, manually starting Xvfb, VNC, noVNC..."));
let launch_cmd = format!("sh -c '\
if command -v start-vnc >/dev/null 2>&1; then \
setsid nohup start-vnc >/tmp/vnc_start.log 2>&1 & \
sleep 2; \
sleep 3; \
else \
export DISPLAY={display} && \
rm -f /tmp/.X0-lock && \
setsid nohup Xvfb {display} -screen 0 {res} >/tmp/xvfb.log 2>&1 & \
sleep 1 && \
setsid nohup fluxbox >/tmp/fluxbox.log 2>&1 & \
setsid nohup env DISPLAY={display} fluxbox >/tmp/fluxbox.log 2>&1 & \
sleep 1 && \
setsid nohup x11vnc -display {display} -forever -shared -nopw -rfbport {vnc_port} >/tmp/x11vnc.log 2>&1 & \
setsid nohup env DISPLAY={display} x11vnc -display {display} -forever -shared -nopw -rfbport {vnc_port} >/tmp/x11vnc.log 2>&1 & \
sleep 1 && \
setsid nohup websockify --web /usr/share/novnc 0.0.0.0:{web_port} localhost:{vnc_port} >/tmp/websockify.log 2>&1 & \
sleep 1; \
sleep 2; \
fi'",
display = DISPLAY_ID,
res = SCREEN_RESOLUTION,
Expand Down
6 changes: 3 additions & 3 deletions crates/flock-tools/src/tools/sandbox/browser/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ pub async fn handle_interactive(
if ! python3 -c 'import socket; s = socket.socket(); s.connect((\"127.0.0.1\", 9222))' >/dev/null 2>&1; then \
echo 'Starting chromium exposing 9222 debugger port on DISPLAY={display}...' && \
if command -v chromium >/dev/null 2>&1; then \
setsid nohup chromium --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-software-rasterizer >/tmp/interactive_chrome.log 2>&1 & \
setsid nohup chromium --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-dev-shm-usage --no-first-run --no-default-browser-check --window-size=1280,1024 >/tmp/interactive_chrome.log 2>&1 & \
elif command -v chromium-browser >/dev/null 2>&1; then \
setsid nohup chromium-browser --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-software-rasterizer >/tmp/interactive_chrome.log 2>&1 & \
setsid nohup chromium-browser --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-dev-shm-usage --no-first-run --no-default-browser-check --window-size=1280,1024 >/tmp/interactive_chrome.log 2>&1 & \
elif command -v google-chrome >/dev/null 2>&1; then \
setsid nohup google-chrome --no-sandbox --remote-debugging-port=9222 --disable-gpu >/tmp/interactive_chrome.log 2>&1 & \
setsid nohup google-chrome --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-dev-shm-usage --no-first-run --no-default-browser-check --window-size=1280,1024 >/tmp/interactive_chrome.log 2>&1 & \
fi && \
sleep 3; \
fi",
Expand Down
12 changes: 9 additions & 3 deletions crates/flock-tools/src/tools/sandbox/browser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,20 @@ async fn execute_regular_action(
"export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers && \
export DISPLAY={display} && \
mkdir -p /tmp && echo '{}' | base64 -d > /tmp/run_browser.py && \
if python3 -c 'import socket; s = socket.socket(); s.connect((\"127.0.0.1\", 9222))' >/dev/null 2>&1; then \
if ps aux 2>/dev/null | grep -v grep | grep -q 'disable-software-rasterizer'; then \
echo 'Detected old chromium with --disable-software-rasterizer, restarting with correct flags...' && \
pkill -f 'remote-debugging-port=9222' 2>/dev/null; sleep 1; \
fi; \
fi; \
if ! python3 -c 'import socket; s = socket.socket(); s.connect((\"127.0.0.1\", 9222))' >/dev/null 2>&1; then \
echo 'Starting headful chromium with remote debugging on DISPLAY={display}...' && \
if command -v chromium >/dev/null 2>&1; then \
setsid nohup chromium --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-software-rasterizer >/tmp/headless_chrome.log 2>&1 & \
setsid nohup env DISPLAY={display} chromium --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-dev-shm-usage --no-first-run --no-default-browser-check --window-size=1280,1024 >/tmp/headless_chrome.log 2>&1 & \
elif command -v chromium-browser >/dev/null 2>&1; then \
setsid nohup chromium-browser --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-software-rasterizer >/tmp/headless_chrome.log 2>&1 & \
setsid nohup env DISPLAY={display} chromium-browser --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-dev-shm-usage --no-first-run --no-default-browser-check --window-size=1280,1024 >/tmp/headless_chrome.log 2>&1 & \
elif command -v google-chrome >/dev/null 2>&1; then \
setsid nohup google-chrome --no-sandbox --remote-debugging-port=9222 --disable-gpu >/tmp/headless_chrome.log 2>&1 & \
setsid nohup env DISPLAY={display} google-chrome --no-sandbox --remote-debugging-port=9222 --disable-gpu --disable-dev-shm-usage --no-first-run --no-default-browser-check --window-size=1280,1024 >/tmp/headless_chrome.log 2>&1 & \
fi && \
sleep 3; \
fi; \
Expand Down
Loading
Loading