2182 lines
86 KiB
Bash
2182 lines
86 KiB
Bash
#!/bin/bash
|
||
|
||
# ============================================================================
|
||
# 📝 带时间戳的日志函数
|
||
# 所有日志输出都会自动添加 UTC 时间前缀,格式与 agent_runner 一致
|
||
# 格式:YYYY-MM-DDTHH:MM:SS.ffffffZ(ISO 8601 UTC)
|
||
# 注意:日志函数必须在脚本最开头定义,因为后面的代码会立即使用它们
|
||
# ============================================================================
|
||
function log() {
|
||
echo "$(date -u '+%Y-%m-%dT%H:%M:%S.%6NZ') INFO $*"
|
||
}
|
||
|
||
function log_info() {
|
||
echo "$(date -u '+%Y-%m-%dT%H:%M:%S.%6NZ') INFO ℹ️ $*"
|
||
}
|
||
|
||
function log_success() {
|
||
echo "$(date -u '+%Y-%m-%dT%H:%M:%S.%6NZ') INFO ✓ $*"
|
||
}
|
||
|
||
function log_warn() {
|
||
echo "$(date -u '+%Y-%m-%dT%H:%M:%S.%6NZ') WARN ⚠️ $*"
|
||
}
|
||
|
||
function log_error() {
|
||
echo "$(date -u '+%Y-%m-%dT%H:%M:%S.%6NZ') ERROR ❌ $*"
|
||
}
|
||
|
||
# ============================================================================
|
||
# 🎯 容器日志持久化设置
|
||
# 将日志输出到挂载的持久化目录,即使容器崩溃也能保留日志
|
||
# ============================================================================
|
||
CONTAINER_LOGS_DIR="${CONTAINER_LOGS_DIR:-/app/container-logs}"
|
||
CONTAINER_LOG_NAME="${CONTAINER_LOG_NAME:-unknown}"
|
||
|
||
# 检查日志目录是否可用(由主容器挂载)
|
||
if [ -d "$CONTAINER_LOGS_DIR" ] && [ -w "$CONTAINER_LOGS_DIR" ]; then
|
||
# 创建日志文件
|
||
STARTUP_LOG="$CONTAINER_LOGS_DIR/startup.log"
|
||
ERROR_LOG="$CONTAINER_LOGS_DIR/error.log"
|
||
AGENT_LOG="$CONTAINER_LOGS_DIR/agent.log"
|
||
|
||
# 记录容器启动信息
|
||
echo "============================================" >> "$STARTUP_LOG"
|
||
echo "Container startup at $(date '+%Y-%m-%d %H:%M:%S %Z')" >> "$STARTUP_LOG"
|
||
echo "Container hostname: $(hostname)" >> "$STARTUP_LOG"
|
||
echo "Log directory name: $CONTAINER_LOG_NAME" >> "$STARTUP_LOG"
|
||
echo "USER_ID: ${USER_ID:-not set}" >> "$STARTUP_LOG"
|
||
echo "PROJECT_ID: ${PROJECT_ID:-not set}" >> "$STARTUP_LOG"
|
||
echo "============================================" >> "$STARTUP_LOG"
|
||
|
||
# 将标准输出和错误输出同时发送到终端和日志文件
|
||
# stdout -> tee -> STARTUP_LOG
|
||
# stderr -> tee -> ERROR_LOG
|
||
exec > >(tee -a "$STARTUP_LOG") 2> >(tee -a "$ERROR_LOG" >&2)
|
||
|
||
log "容器日志持久化已启用: $CONTAINER_LOGS_DIR"
|
||
echo " - startup.log: 启动日志"
|
||
echo " - error.log: 错误日志"
|
||
echo " - agent.log: Agent 运行日志 (如有)"
|
||
|
||
# 导出日志路径供其他进程使用
|
||
export CONTAINER_STARTUP_LOG="$STARTUP_LOG"
|
||
export CONTAINER_ERROR_LOG="$ERROR_LOG"
|
||
export CONTAINER_AGENT_LOG="$AGENT_LOG"
|
||
else
|
||
log_warn " 容器日志目录不可用,使用默认输出"
|
||
fi
|
||
|
||
|
||
# ============================================================================
|
||
# 🎯 动态时区设置(支持通过 TZ 环境变量自定义时区)
|
||
# 默认时区为 Asia/Shanghai(在 Dockerfile 中配置)
|
||
# 启动时如果检测到 TZ 环境变量,则更新系统时区
|
||
# 使用方式: docker run -e TZ=America/New_York your-image
|
||
# ============================================================================
|
||
function initialize_timezone() {
|
||
# 如果 TZ 环境变量未设置或为空,保持默认时区(Asia/Shanghai)
|
||
if [ -z "$TZ" ]; then
|
||
log "Using default timezone: Asia/Shanghai"
|
||
return 0
|
||
fi
|
||
|
||
log "Setting timezone to: $TZ"
|
||
|
||
# 检查时区文件是否存在
|
||
if [ -f "/usr/share/zoneinfo/$TZ" ]; then
|
||
# 更新 /etc/localtime 软链接
|
||
ln -sf "/usr/share/zoneinfo/$TZ" /etc/localtime
|
||
# 更新 /etc/timezone 文件
|
||
echo "$TZ" > /etc/timezone
|
||
log_success "Timezone set to $TZ"
|
||
else
|
||
log_warn " Invalid timezone: $TZ (file /usr/share/zoneinfo/$TZ not found)"
|
||
echo " Available timezones can be found in /usr/share/zoneinfo/"
|
||
echo " Keeping default timezone: Asia/Shanghai"
|
||
fi
|
||
}
|
||
|
||
# ============================================================================
|
||
# 🔧 通用等待 Helper 函数(用于替代固定 sleep 阻塞)
|
||
# 使用轮询机制,可提前返回,大幅减少启动时间
|
||
# 注意:使用纯 bash 整数运算,无需依赖 bc 命令
|
||
# ============================================================================
|
||
|
||
# 等待进程启动,最长等待 $2 秒(默认 10 秒)
|
||
# 用法: wait_for_process "process_name" [timeout_seconds]
|
||
function wait_for_process() {
|
||
local process_name="$1"
|
||
local timeout="${2:-10}"
|
||
local interval_ms=200 # 200ms 间隔
|
||
local max_iterations=$((timeout * 1000 / interval_ms))
|
||
local i=0
|
||
|
||
while ! pgrep -x "$process_name" >/dev/null 2>&1; do
|
||
sleep 0.2
|
||
i=$((i + 1))
|
||
if [ $i -ge $max_iterations ]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 等待进程启动(支持模式匹配),最长等待 $2 秒(默认 10 秒)
|
||
# 用法: wait_for_process_pattern "pattern" [timeout_seconds]
|
||
function wait_for_process_pattern() {
|
||
local pattern="$1"
|
||
local timeout="${2:-10}"
|
||
local interval_ms=200
|
||
local max_iterations=$((timeout * 1000 / interval_ms))
|
||
local i=0
|
||
|
||
while ! pgrep -f "$pattern" >/dev/null 2>&1; do
|
||
sleep 0.2
|
||
i=$((i + 1))
|
||
if [ $i -ge $max_iterations ]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 等待端口可用,最长等待 $3 秒(默认 10 秒)
|
||
# 用法: wait_for_port host port [timeout_seconds]
|
||
function wait_for_port() {
|
||
local host="$1"
|
||
local port="$2"
|
||
local timeout="${3:-10}"
|
||
local interval_ms=300 # 300ms 间隔
|
||
local max_iterations=$((timeout * 1000 / interval_ms))
|
||
local i=0
|
||
|
||
while ! nc -z "$host" "$port" 2>/dev/null; do
|
||
sleep 0.3
|
||
i=$((i + 1))
|
||
if [ $i -ge $max_iterations ]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 等待 noVNC WebSocket 服务真正就绪
|
||
# 通过 curl 发送 WebSocket 升级请求,验证 /websockify 路径可用
|
||
# 比 HTTP 检查更可靠(HTTP 200 ≠ WebSocket 代理可用)
|
||
# 比 Python websockets 更轻量(无需启动 Python 解释器)
|
||
# 用法: wait_for_novnc_ready [timeout_seconds]
|
||
function wait_for_novnc_ready() {
|
||
local timeout="${1:-10}"
|
||
local i=0
|
||
|
||
log "Waiting for noVNC WebSocket service to be fully ready..."
|
||
|
||
while true; do
|
||
# 发送 WebSocket 升级请求到 /websockify 路径
|
||
# 返回 101 Switching Protocols 说明 WebSocket 代理真正可用
|
||
#
|
||
# 注意:不要加 || echo "000"!
|
||
# curl 收到 101 后连接升级为 WebSocket,--max-time 超时导致退出码非零
|
||
# 如果加了 ||,echo 的输出会拼在 curl -w 输出后面,变成 "101000" 而非 "101"
|
||
# curl 本身在连接失败时 -w "%{http_code}" 已经会输出 "000",不需要 fallback
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
--max-time 2 \
|
||
-H "Upgrade: websocket" \
|
||
-H "Connection: Upgrade" \
|
||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||
-H "Sec-WebSocket-Version: 13" \
|
||
"http://localhost:6080/websockify" 2>/dev/null)
|
||
|
||
if [ "$http_code" = "101" ]; then
|
||
log_success "noVNC WebSocket service is ready (WebSocket upgrade 101)"
|
||
return 0
|
||
fi
|
||
|
||
sleep 0.5
|
||
i=$((i + 1))
|
||
if [ $i -ge $((timeout * 2)) ]; then
|
||
log_warn "noVNC WebSocket service not ready within ${timeout}s timeout (last HTTP code: $http_code)"
|
||
return 1
|
||
fi
|
||
done
|
||
}
|
||
|
||
# 等待文件存在,最长等待 $2 秒(默认 5 秒)
|
||
# 用法: wait_for_file "filepath" [timeout_seconds]
|
||
function wait_for_file() {
|
||
local filepath="$1"
|
||
local timeout="${2:-5}"
|
||
local interval_ms=200
|
||
local max_iterations=$((timeout * 1000 / interval_ms))
|
||
local i=0
|
||
|
||
while [ ! -f "$filepath" ]; do
|
||
sleep 0.2
|
||
i=$((i + 1))
|
||
if [ $i -ge $max_iterations ]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 等待进程终止,最长等待 $2 秒(默认 5 秒)
|
||
# 用法: wait_for_process_exit "process_name" [timeout_seconds]
|
||
function wait_for_process_exit() {
|
||
local process_name="$1"
|
||
local timeout="${2:-5}"
|
||
local interval_ms=200
|
||
local max_iterations=$((timeout * 1000 / interval_ms))
|
||
local i=0
|
||
|
||
while pgrep -x "$process_name" >/dev/null 2>&1; do
|
||
sleep 0.2
|
||
i=$((i + 1))
|
||
if [ $i -ge $max_iterations ]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 初始化时区(在日志设置之后、用户目录初始化之前)
|
||
initialize_timezone
|
||
|
||
# ============================================================================
|
||
# 🧹 清理旧的 VNC 就绪标记文件(容器重启时可能残留)
|
||
# 确保 VNC 状态查询 API 返回准确的状态
|
||
# ============================================================================
|
||
rm -f /tmp/vnc_ready /tmp/novnc_port_ready /tmp/wallpaper_ready
|
||
log "Cleaned up stale VNC ready markers (if any)"
|
||
|
||
# ============================================================================
|
||
# 🎯 注意:所有服务均以 root 用户运行
|
||
# ============================================================================
|
||
# 不再需要 UID/GID 匹配,因为 root 用户拥有所有权限
|
||
# HOME 环境变量设置为 /home/user,缓存和配置仍存放在该目录
|
||
# ============================================================================
|
||
|
||
|
||
# ============================================================================
|
||
# 🎯 用户主目录初始化(解决挂载空目录导致的花屏和图标消失问题)
|
||
# 当宿主机目录挂载到 /home/user 时,镜像中预置的配置会被覆盖
|
||
# 此函数从骨架目录 /etc/skel-user-desktop 恢复必要的配置文件
|
||
# ============================================================================
|
||
function initialize_user_home() {
|
||
log "Initializing user home directory..."
|
||
|
||
local SKEL_DIR="/etc/skel-user-desktop"
|
||
local USER_HOME="/home/user"
|
||
|
||
# 检查骨架目录是否存在
|
||
if [ ! -d "$SKEL_DIR" ]; then
|
||
log_warn " Skeleton directory not found: $SKEL_DIR"
|
||
return 1
|
||
fi
|
||
|
||
# 检查 /home/user 是否被外部挂载覆盖(通过检查关键目录是否存在)
|
||
local need_restore=false
|
||
|
||
# 检查关键目录/文件是否存在(任一不存在则需要恢复)
|
||
# 注意:.config/xfce4 包含 XFCE Panel 配置,缺失会导致底部 Dock 栏图标消失
|
||
if [ ! -d "$USER_HOME/Desktop" ] || \
|
||
[ ! -f "$USER_HOME/.bashrc" ] || \
|
||
[ ! -f "$USER_HOME/.bunfig.toml" ] || \
|
||
[ ! -d "$USER_HOME/.claude" ] || \
|
||
[ ! -d "$USER_HOME/.config/xfce4" ]; then
|
||
need_restore=true
|
||
log "Detected empty or incomplete user home directory (likely mounted)"
|
||
echo " Missing: $([ ! -d "$USER_HOME/Desktop" ] && echo 'Desktop ')$([ ! -f "$USER_HOME/.bashrc" ] && echo '.bashrc ')$([ ! -f "$USER_HOME/.bunfig.toml" ] && echo '.bunfig.toml ')$([ ! -d "$USER_HOME/.claude" ] && echo '.claude ')$([ ! -d "$USER_HOME/.config/xfce4" ] && echo '.config/xfce4 ')"
|
||
fi
|
||
|
||
if [ "$need_restore" = true ]; then
|
||
log "Restoring user configuration from skeleton directory..."
|
||
|
||
# 创建必要的目录结构
|
||
mkdir -p "$USER_HOME/.config" "$USER_HOME/.local/share" "$USER_HOME/.cache"
|
||
mkdir -p "$USER_HOME/Desktop"
|
||
|
||
# ========== Desktop 目录 - 强制覆盖桌面图标(防止坏软链接)==========
|
||
if [ -d "$SKEL_DIR/Desktop" ]; then
|
||
# 先删除现有的 .desktop 文件(可能是损坏的软链接)
|
||
rm -f "$USER_HOME/Desktop/"*.desktop 2>/dev/null || true
|
||
# 强制复制桌面图标
|
||
cp -a "$SKEL_DIR/Desktop/"*.desktop "$USER_HOME/Desktop/" 2>/dev/null || true
|
||
# 设置可执行权限
|
||
chmod +x "$USER_HOME/Desktop/"*.desktop 2>/dev/null || true
|
||
log_success " Desktop icons restored (forced overwrite)"
|
||
fi
|
||
|
||
# ========== .bashrc - 强制覆盖 ==========
|
||
if [ -f "$SKEL_DIR/.bashrc" ]; then
|
||
cp -a "$SKEL_DIR/.bashrc" "$USER_HOME/.bashrc"
|
||
log_success " .bashrc restored (forced overwrite)"
|
||
fi
|
||
|
||
# ========== .config 目录 - 强制覆盖(保留 Chromium 用户数据)==========
|
||
if [ -d "$SKEL_DIR/.config" ]; then
|
||
# 1. 备份现有的 Chromium 用户数据(书签、历史记录等)
|
||
local chromium_backup=""
|
||
if [ -d "$USER_HOME/.config/chromium" ]; then
|
||
chromium_backup=$(mktemp -d)
|
||
cp -a "$USER_HOME/.config/chromium" "$chromium_backup/" 2>/dev/null || true
|
||
log_success " Chromium user data backed up"
|
||
fi
|
||
|
||
# 2. 强制覆盖整个 .config 目录
|
||
cp -a "$SKEL_DIR/.config/." "$USER_HOME/.config/" 2>/dev/null || true
|
||
log_success " .config directory restored (forced overwrite)"
|
||
|
||
# 3. 还原 Chromium 用户数据(覆盖骨架目录的默认配置)
|
||
if [ -n "$chromium_backup" ] && [ -d "$chromium_backup/chromium" ]; then
|
||
cp -a "$chromium_backup/chromium/." "$USER_HOME/.config/chromium/" 2>/dev/null || true
|
||
rm -rf "$chromium_backup"
|
||
log_success " Chromium user data restored"
|
||
fi
|
||
fi
|
||
|
||
# ========== .local 目录 - 强制覆盖 ==========
|
||
if [ -d "$SKEL_DIR/.local" ]; then
|
||
cp -a "$SKEL_DIR/.local/." "$USER_HOME/.local/" 2>/dev/null || true
|
||
log_success " .local directory restored (forced overwrite)"
|
||
fi
|
||
|
||
# ========== .bunfig.toml - 强制覆盖 ==========
|
||
if [ -f "$SKEL_DIR/.bunfig.toml" ]; then
|
||
cp -a "$SKEL_DIR/.bunfig.toml" "$USER_HOME/.bunfig.toml"
|
||
log_success " .bunfig.toml restored (forced overwrite)"
|
||
fi
|
||
|
||
# ========== .npmrc - 强制覆盖(pnpm 配置)==========
|
||
if [ -f "$SKEL_DIR/.npmrc" ]; then
|
||
cp -a "$SKEL_DIR/.npmrc" "$USER_HOME/.npmrc"
|
||
log_success " .npmrc restored (forced overwrite)"
|
||
fi
|
||
|
||
# ========== .claude 目录 - 不覆盖(保留用户配置)==========
|
||
if [ -d "$SKEL_DIR/.claude" ]; then
|
||
mkdir -p "$USER_HOME/.claude"
|
||
cp -an "$SKEL_DIR/.claude/." "$USER_HOME/.claude/" 2>/dev/null || true
|
||
log_success " .claude directory restored (preserve existing)"
|
||
fi
|
||
|
||
# .cache 目录 - 恢复工具缓存配置(bun, uv, pnpm)
|
||
if [ -d "$SKEL_DIR/.cache" ]; then
|
||
mkdir -p "$USER_HOME/.cache"
|
||
# 只复制目录结构,不复制实际缓存内容(避免大量复制)
|
||
for cache_subdir in bun uv pnpm; do
|
||
if [ -d "$SKEL_DIR/.cache/$cache_subdir" ] && [ ! -d "$USER_HOME/.cache/$cache_subdir" ]; then
|
||
mkdir -p "$USER_HOME/.cache/$cache_subdir"
|
||
log_success " .cache/$cache_subdir directory created"
|
||
fi
|
||
done
|
||
fi
|
||
|
||
log_success "User home directory initialized from skeleton"
|
||
else
|
||
log_success "User home directory already initialized"
|
||
fi
|
||
|
||
# ========== 额外保护:确保 XFCE Panel 配置始终有效 ==========
|
||
# 即使上面的恢复逻辑没有触发,也检查 Panel 配置是否完整
|
||
# 关键修复:XFCE 会在运行时重写 panel.xml,可能保存损坏的配置
|
||
# 因此必须检查内容有效性,而不仅仅是文件是否存在
|
||
local XFCE_PANEL_XML="$USER_HOME/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml"
|
||
local XFCE_PANEL_SYSTEM="/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml"
|
||
local panel_corrupted=false
|
||
|
||
# 检查 panel.xml 是否存在且有效
|
||
if [ ! -f "$XFCE_PANEL_XML" ] || [ ! -s "$XFCE_PANEL_XML" ]; then
|
||
panel_corrupted=true
|
||
log "XFCE Panel config missing or empty"
|
||
elif ! grep -q 'value="launcher"' "$XFCE_PANEL_XML" 2>/dev/null; then
|
||
# 检查 panel.xml 是否包含有效的 launcher 定义
|
||
# 如果 plugin-17 等不是 type="string" value="launcher",说明配置被 XFCE 重写损坏了
|
||
panel_corrupted=true
|
||
log "XFCE Panel config corrupted (launcher definitions missing)"
|
||
elif ! grep -q 'xfce4-terminal-emulator.desktop' "$XFCE_PANEL_XML" 2>/dev/null; then
|
||
# 检查是否包含 launcher items(.desktop 文件引用)
|
||
panel_corrupted=true
|
||
log "XFCE Panel config corrupted (launcher items empty)"
|
||
fi
|
||
|
||
if [ "$panel_corrupted" = true ]; then
|
||
log "Restoring XFCE Panel config from system..."
|
||
mkdir -p "$(dirname "$XFCE_PANEL_XML")"
|
||
if [ -f "$XFCE_PANEL_SYSTEM" ]; then
|
||
cp -f "$XFCE_PANEL_SYSTEM" "$XFCE_PANEL_XML"
|
||
log_success " xfce4-panel.xml restored from system config (forced overwrite)"
|
||
elif [ -f "$SKEL_DIR/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml" ]; then
|
||
cp -f "$SKEL_DIR/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml" "$XFCE_PANEL_XML"
|
||
log_success " xfce4-panel.xml restored from skeleton (forced overwrite)"
|
||
fi
|
||
else
|
||
log_success "XFCE Panel config is valid"
|
||
fi
|
||
|
||
# ========== 🔥 关键修复:先替换壁纸文件,再加载配置(解决壁纸竞态条件) ==========
|
||
# 必须在 xfce4-desktop.xml 加载之前完成壁纸文件替换!
|
||
# 否则 xfdesktop 启动时会读取到默认壁纸并缓存,后续设置不生效
|
||
local CUSTOM_WALLPAPER="/app/assets/wallpaper.jpeg"
|
||
local XFCE_WALLPAPER="/usr/share/backgrounds/xfce/wallpaper.jpeg"
|
||
local NOVNC_BG="/opt/noVNC/app/images/bg.jpg"
|
||
|
||
if [ -f "$CUSTOM_WALLPAPER" ]; then
|
||
log "Found custom wallpaper at $CUSTOM_WALLPAPER, applying BEFORE xfdesktop starts..."
|
||
|
||
# 1. 应用到 XFCE 桌面(备份原始文件)
|
||
if [ ! -f "${XFCE_WALLPAPER}.original" ]; then
|
||
cp "$XFCE_WALLPAPER" "${XFCE_WALLPAPER}.original" 2>/dev/null || true
|
||
fi
|
||
cp "$CUSTOM_WALLPAPER" "$XFCE_WALLPAPER"
|
||
log_success " XFCE wallpaper applied EARLY: $CUSTOM_WALLPAPER -> $XFCE_WALLPAPER"
|
||
|
||
# 2. 应用到 noVNC 网页背景(同一图片,不同文件名)
|
||
if [ ! -f "${NOVNC_BG}.original" ]; then
|
||
cp "$NOVNC_BG" "${NOVNC_BG}.original" 2>/dev/null || true
|
||
fi
|
||
cp "$CUSTOM_WALLPAPER" "$NOVNC_BG"
|
||
log_success " noVNC background applied EARLY: $CUSTOM_WALLPAPER -> $NOVNC_BG"
|
||
else
|
||
log "No custom wallpaper found at $CUSTOM_WALLPAPER (using defaults)"
|
||
fi
|
||
|
||
# ========== 修复:预加载 XFCE 桌面壁纸配置(防止启动黑屏和缩放问题) ==========
|
||
# 复制系统配置到用户目录,包含所有 monitor 路径和缩放设置 (image-style=5)
|
||
local XFCE_DESKTOP_XML="$USER_HOME/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml"
|
||
local XFCE_DESKTOP_SYSTEM="/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml"
|
||
|
||
if [ ! -f "$XFCE_DESKTOP_XML" ] && [ -f "$XFCE_DESKTOP_SYSTEM" ]; then
|
||
mkdir -p "$(dirname "$XFCE_DESKTOP_XML")"
|
||
cp -f "$XFCE_DESKTOP_SYSTEM" "$XFCE_DESKTOP_XML"
|
||
log_success " xfce4-desktop.xml pre-configured from system (fixes wallpaper scaling)"
|
||
fi
|
||
|
||
# 确保 Panel launcher 目录存在且内容完整(强制恢复)
|
||
# 注意:每次启动都检查并恢复,防止用户删除后 XFCE 保存损坏状态
|
||
local XFCE_PANEL_DIR="$USER_HOME/.config/xfce4/panel"
|
||
for launcher_id in 17 18 19 20; do
|
||
local launcher_dir="$XFCE_PANEL_DIR/launcher-$launcher_id"
|
||
local system_launcher="/etc/xdg/xfce4/panel/launcher-$launcher_id"
|
||
if [ -d "$system_launcher" ]; then
|
||
# 检查 launcher 目录是否存在且包含 .desktop 文件
|
||
if [ ! -d "$launcher_dir" ] || [ -z "$(ls -A "$launcher_dir" 2>/dev/null)" ]; then
|
||
mkdir -p "$launcher_dir"
|
||
cp -f "$system_launcher/"* "$launcher_dir/" 2>/dev/null || true
|
||
log_success " launcher-$launcher_id restored (forced)"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# ========== 确保 GTK CSS 配置存在(隐藏 Thunar root 警告) ==========
|
||
# 同时配置 /root 和 /home/user,因为:
|
||
# - 以 root 用户运行时,某些进程可能读取 /root/.config
|
||
# - 设置 HOME=/home/user 后,大部分进程读取 /home/user/.config
|
||
# 注意:只使用 GTK 3.0 支持的 CSS 属性,避免解析警告
|
||
local GTK_CSS_CONTENT='/* Hide Thunar root warnings - GTK 3.0 compatible */
|
||
.thunar-window infobar.warning {
|
||
min-height: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
opacity: 0;
|
||
}
|
||
.thunar-window infobar.warning * {
|
||
min-height: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
opacity: 0;
|
||
}
|
||
.thunar-window infobar.warning button {
|
||
min-height: 0;
|
||
min-width: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
opacity: 0;
|
||
}
|
||
infobar.warning {
|
||
min-height: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
opacity: 0;
|
||
}
|
||
infobar.warning * {
|
||
min-height: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
opacity: 0;
|
||
}
|
||
/* Hide XFCE root warning - using opacity instead of display */
|
||
.root-warning {
|
||
opacity: 0 !important;
|
||
min-height: 0;
|
||
}
|
||
.root-warning * {
|
||
opacity: 0 !important;
|
||
min-height: 0;
|
||
}
|
||
'
|
||
# 为 /root 创建配置
|
||
mkdir -p /root/.config/gtk-3.0
|
||
echo "$GTK_CSS_CONTENT" > /root/.config/gtk-3.0/gtk.css
|
||
log_success " GTK CSS created for /root"
|
||
|
||
# 为 /home/user 创建配置
|
||
mkdir -p "$USER_HOME/.config/gtk-3.0"
|
||
echo "$GTK_CSS_CONTENT" > "$USER_HOME/.config/gtk-3.0/gtk.css"
|
||
log_success " GTK CSS created for $USER_HOME"
|
||
|
||
# ========== 抑制 gnome-keyring 模块加载告警 ==========
|
||
# 容器中未安装 gnome-keyring,配置 GTK 不尝试加载该模块
|
||
# 创建 GTK 模块配置,禁用 gnome-keyring-pkcs11
|
||
mkdir -p /root/.config/gtk-3.0
|
||
cat > /root/.config/gtk-3.0/settings.ini <<'EOF'
|
||
[Settings]
|
||
gtk-modules=
|
||
EOF
|
||
|
||
mkdir -p "$USER_HOME/.config/gtk-3.0"
|
||
cat > "$USER_HOME/.config/gtk-3.0/settings.ini" <<'EOF'
|
||
[Settings]
|
||
gtk-modules=
|
||
EOF
|
||
log_success " GTK module config created (gnome-keyring disabled)"
|
||
|
||
# ========== 设置 Chromium 为默认浏览器(解决 xdg-open 无法打开浏览器问题)==========
|
||
log "Configuring Chromium as default web browser..."
|
||
|
||
# 创建用户级 mimeapps.list(强制覆盖,确保默认浏览器设置正确)
|
||
mkdir -p "$USER_HOME/.config" "$USER_HOME/.local/share/applications"
|
||
|
||
cat > "$USER_HOME/.config/mimeapps.list" <<'EOF'
|
||
[Default Applications]
|
||
text/html=chromium.desktop
|
||
text/xml=chromium.desktop
|
||
application/xhtml+xml=chromium.desktop
|
||
application/xml=chromium.desktop
|
||
application/rss+xml=chromium.desktop
|
||
application/rdf+xml=chromium.desktop
|
||
x-scheme-handler/http=chromium.desktop
|
||
x-scheme-handler/https=chromium.desktop
|
||
x-scheme-handler/ftp=chromium.desktop
|
||
x-scheme-handler/chrome=chromium.desktop
|
||
x-scheme-handler/about=chromium.desktop
|
||
x-scheme-handler/unknown=chromium.desktop
|
||
|
||
[Added Associations]
|
||
text/html=chromium.desktop;
|
||
x-scheme-handler/http=chromium.desktop;
|
||
x-scheme-handler/https=chromium.desktop;
|
||
EOF
|
||
|
||
# 同时创建 ~/.local/share/applications/mimeapps.list(某些 xdg 工具读取这个位置)
|
||
cp "$USER_HOME/.config/mimeapps.list" "$USER_HOME/.local/share/applications/mimeapps.list"
|
||
|
||
# 使用 xdg-settings 设置默认浏览器(需要在 X11 环境启动后才能完全生效)
|
||
# 这里先创建配置文件,xdg-settings 会在 DISPLAY 环境变量存在时使用
|
||
export BROWSER="/usr/bin/chromium-browser-launcher"
|
||
|
||
log_success " Chromium set as default web browser (mimeapps.list)"
|
||
log_success " BROWSER env set to: $BROWSER"
|
||
|
||
# ========== 修复挂载目录的权限(优化版 - 避免递归遍历大量文件) ==========
|
||
# 优化说明:
|
||
# 1. 容器以 root 身份运行,通过 HOME=/home/user 设置环境变量
|
||
# 2. root 用户可以访问任何文件,不需要递归 chown
|
||
# 3. 只需要确保关键目录的基本权限即可
|
||
log "Fixing permissions for mounted directories (optimized)..."
|
||
|
||
# 确保必要目录存在
|
||
mkdir -p "$USER_HOME/.cache" /app /tmp/mesa_shader_cache "${CONTAINER_LOGS_DIR:-/app/container-logs}"
|
||
|
||
# ========== 方案 1: 只修复顶层目录所有权(非递归,<0.1秒) ==========
|
||
log "Fixing ownership for top-level directories (non-recursive)..."
|
||
chown user:user "$USER_HOME" 2>/dev/null || true
|
||
chown user:user "$USER_HOME/.config" 2>/dev/null || true
|
||
chown user:user "$USER_HOME/.cache" 2>/dev/null || true
|
||
chown user:user "$USER_HOME/Desktop" 2>/dev/null || true
|
||
|
||
# ========== 方案 2: 只递归修复 XFCE 配置目录(文件少,~0.1秒) ==========
|
||
# XFCE 配置文件需要正确的所有者才能被 xfce4-session 正确加载
|
||
# 同时设置 other 读权限让 root 用户也能访问(一次性完成,避免重复遍历)
|
||
if [ -d "$USER_HOME/.config/xfce4" ]; then
|
||
find "$USER_HOME/.config/xfce4" \( -type f -o -type d \) \
|
||
-exec chown user:user {} + \
|
||
-exec chmod o+rX {} + 2>/dev/null || true
|
||
log_success " XFCE config ownership and permissions fixed"
|
||
fi
|
||
|
||
# ========== 方案 3: 通过 chmod 让 root 用户也能访问(容器内以 root 运行) ==========
|
||
# 由于容器以 root 运行,只需要确保 other 有读权限即可
|
||
# 为了安全性和性能,只对必要的目录递归处理
|
||
log "Setting read permissions for root access..."
|
||
|
||
# Desktop 目录递归处理(文件少)
|
||
if [ -d "$USER_HOME/Desktop" ]; then
|
||
chmod -R o+rX "$USER_HOME/Desktop" 2>/dev/null || true
|
||
fi
|
||
|
||
# .cache 和 .local 可能包含大量文件,只修复顶层目录(非递归)
|
||
for dir in "$USER_HOME/.cache" "$USER_HOME/.local" "$USER_HOME/.config"; do
|
||
if [ -d "$dir" ]; then
|
||
chmod o+rX "$dir" 2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# ========== 保护敏感目录(如果存在)==========
|
||
# 确保 SSH 私钥等敏感文件权限严格
|
||
if [ -d "$USER_HOME/.ssh" ]; then
|
||
chmod 700 "$USER_HOME/.ssh" 2>/dev/null || true
|
||
find "$USER_HOME/.ssh" -type f -exec chmod 600 {} \; 2>/dev/null || true
|
||
log_success " .ssh directory protected (700/600)"
|
||
fi
|
||
|
||
# ========== /app 目录权限(已在 Dockerfile 中设置,无需 chown) ==========
|
||
# 只确保 bin 目录可执行(使用 find 避免 glob 展开问题)
|
||
if [ -d /app/bin ]; then
|
||
find /app/bin -type f -exec chmod a+x {} + 2>/dev/null || true
|
||
log_success " /app/bin executables set"
|
||
fi
|
||
|
||
# ========== Mesa 着色器缓存(使用 755 权限,符合最小权限原则) ==========
|
||
chmod 755 /tmp/mesa_shader_cache 2>/dev/null || true
|
||
|
||
log_success "Permissions fixed (optimized - no recursive chown on large directories)"
|
||
|
||
# ========== 设置渲染相关环境变量(防止花屏)==========
|
||
# 将 Mesa 着色器缓存移到 /tmp(不受 /home/user 挂载影响)
|
||
export MESA_SHADER_CACHE_DIR="/tmp/mesa_shader_cache"
|
||
export MESA_GLSL_CACHE_DIR="/tmp/mesa_shader_cache"
|
||
# 注意:目录创建和权限设置已在上面的权限修复部分完成 (Line 473, 516)
|
||
|
||
# 将 X 认证文件移到 /tmp
|
||
export XAUTHORITY="/tmp/.Xauthority"
|
||
|
||
log_success "Mesa shader cache configured: /tmp/mesa_shader_cache"
|
||
|
||
# ========== 🖼️ 自定义壁纸替换已提前执行 ==========
|
||
# 壁纸替换逻辑已移到 xfce4-desktop.xml 加载之前执行(见本函数开头)
|
||
# 这样可以确保 xfdesktop 启动时就能读取到正确的壁纸文件
|
||
}
|
||
|
||
function start_vnc_services() {
|
||
log "Starting VNC services (as root)..."
|
||
|
||
# 注意:调用方(主启动子 shell)已通过 xdpyinfo 确认 X11 就绪
|
||
# 此处不再重复等待 X11,直接检查 Xvnc 端口
|
||
|
||
# 1. 等待 Xvnc 端口 5900 就绪(Xvnc 在 start_display_and_desktop 中启动)
|
||
# 如果 5900 不可用,noVNC 代理必然连接失败,直接 fail fast
|
||
if wait_for_port localhost 5900 5; then
|
||
log_success "Xvnc port 5900 is ready"
|
||
else
|
||
log_error "Xvnc port 5900 not ready within timeout, cannot start noVNC"
|
||
return 1
|
||
fi
|
||
|
||
# 2. 启动 noVNC 代理(使用绝对路径,避免 cd 污染日志输出)
|
||
if ! pgrep -f "novnc_proxy" >/dev/null 2>&1; then
|
||
nohup /opt/noVNC/utils/novnc_proxy \
|
||
--vnc localhost:5900 \
|
||
--listen 6080 \
|
||
--web /opt/noVNC \
|
||
--heartbeat 30 \
|
||
> /tmp/novnc.log 2>&1 &
|
||
fi
|
||
|
||
# 3. 等待 noVNC 端口就绪(最长 20 秒)
|
||
if ! wait_for_port localhost 6080 20; then
|
||
log_error "noVNC port 6080 not ready within timeout"
|
||
log_error "noVNC error log:"
|
||
tail -20 /tmp/novnc.log 2>/dev/null || echo 'No error log found'
|
||
return 1
|
||
fi
|
||
log_success "noVNC port 6080 is listening"
|
||
|
||
# 4. 验证 WebSocket 代理真正可用(最长 10 秒)
|
||
# 这是写入 novnc_port_ready 标记的硬性前置条件
|
||
# 如果 WebSocket 升级 (101) 不通过,说明 /websockify 路径不可用
|
||
# 此时绝不能写入标记,否则前端会认为 novnc_ready=true 但实际连不上
|
||
if ! wait_for_novnc_ready 10; then
|
||
log_error "noVNC WebSocket service not ready, will NOT write ready marker"
|
||
log_error "noVNC error log:"
|
||
tail -20 /tmp/novnc.log 2>/dev/null || echo 'No error log found'
|
||
return 1
|
||
fi
|
||
log_success "noVNC WebSocket service is fully ready"
|
||
|
||
# 5. 所有检查通过,写入 noVNC 端口就绪标记
|
||
# 此标记仅在以下条件全部满足时写入:
|
||
# - Xvnc 端口 5900 就绪 (VNC 服务器可用)
|
||
# - noVNC 端口 6080 就绪 (HTTP 服务可用)
|
||
# - WebSocket /websockify 升级返回 101 (浏览器可以真正连接)
|
||
# 最终的 /tmp/vnc_ready 由 wait_and_write_vnc_ready_marker() 在壁纸+桌面也就绪后写入
|
||
log_success "Xvnc server is running on port 5900"
|
||
log_success "noVNC proxy started on port 6080"
|
||
echo " VNC URL: http://localhost:6080/vnc.html?autoconnect=true&resize=scale"
|
||
echo "$(date +%s)" > /tmp/novnc_port_ready
|
||
log_success "noVNC port ready marker written to /tmp/novnc_port_ready"
|
||
return 0
|
||
}
|
||
|
||
function start_display_and_desktop() {
|
||
log "Starting X11 display server and XFCE4 desktop..."
|
||
|
||
# 清理可能存在的X11锁文件和进程
|
||
rm -f /tmp/.X0-lock /tmp/.X11-unix/X0 /tmp/.Xauthority /tmp/dbus-session-env
|
||
pkill -f "Xvnc :0" || true
|
||
pkill -f "xfce4-session" || true
|
||
pkill -f "dbus-daemon" || true
|
||
pkill -f "fcitx5" || true
|
||
|
||
# 确保 /tmp 权限正确且 XAUTHORITY 可写
|
||
touch /tmp/.Xauthority
|
||
chmod 666 /tmp/.Xauthority
|
||
touch /tmp/dbus-session-env
|
||
chmod 666 /tmp/dbus-session-env
|
||
|
||
# ========== 优化:尽早启动 Xvnc (后台) ==========
|
||
# Xvnc 是 TigerVNC 内置的 X server + VNC 服务器
|
||
# 使用 Xvnc 替代 Xvfb + x11vnc 的组合,简化架构
|
||
# 色深使用 24 位,避免某些 Linux 内核上出现花屏
|
||
# FrameRate 30: 限制每秒最大帧数 (默认60),降低到30可减少约50%带宽,日常使用无明显差异
|
||
# 注意: CompressLevel/QualityLevel 是 VNC 客户端参数,不是 Xvnc 服务端参数
|
||
# 真正的压缩配置在 noVNC 客户端侧 (rfb.js 的 compressionLevel/qualityLevel)
|
||
log "Starting Xvnc :0 (background initialization)..."
|
||
HOME=/home/user XAUTHORITY=/tmp/.Xauthority MESA_SHADER_CACHE_DIR=/tmp/mesa_shader_cache Xvnc :0 -geometry 1920x1080 -depth 24 -SecurityTypes None -ac -rfbport 5900 -FrameRate 20 -AlwaysShared >/tmp/xvnc.log 2>&1 &
|
||
|
||
|
||
# ========== 关键修复:清理 Chromium 进程和锁文件 ==========
|
||
log "Cleaning up stale Chromium processes and lock files..."
|
||
|
||
# 1. 强制终止所有遗留的 Chromium 进程
|
||
pkill -9 -f "chromium" || true
|
||
pkill -9 -f "chrome" || true
|
||
|
||
# 2. 设置持久化的 Chromium 数据目录路径
|
||
# 使用用户主目录的标准配置路径(自动持久化)
|
||
CHROMIUM_USER_DATA_DIR="${CHROMIUM_USER_DATA_DIR:-/home/user/.config/chromium}"
|
||
log_success "使用 Chromium 数据目录: $CHROMIUM_USER_DATA_DIR (自动持久化)"
|
||
|
||
# 3. 创建 Chromium 数据目录(如果不存在)
|
||
mkdir -p "$CHROMIUM_USER_DATA_DIR"
|
||
# 修复权限(已在 initialize_user_home() 中统一处理,此处确保目录存在即可)
|
||
# 避免 chmod -R 777 造成安全风险
|
||
|
||
# 4. 导出环境变量供后续进程使用
|
||
export CHROMIUM_USER_DATA_DIR
|
||
echo "export CHROMIUM_USER_DATA_DIR='${CHROMIUM_USER_DATA_DIR}'" >> /etc/environment
|
||
echo "export CHROMIUM_USER_DATA_DIR='${CHROMIUM_USER_DATA_DIR}'" >> /etc/profile.d/chromium-env.sh
|
||
|
||
# 4.1 agent-browser 使用独立 profile,避免与 MCP 的 Chromium 争抢锁
|
||
AGENT_BROWSER_PROFILE="${AGENT_BROWSER_PROFILE:-/home/user/.config/agent-browser/chromium}"
|
||
export AGENT_BROWSER_PROFILE
|
||
log_success "agent-browser configured with isolated profile: $AGENT_BROWSER_PROFILE"
|
||
|
||
# 5. 清理 Chromium profile 锁文件(SingletonLock)
|
||
if [ -d "$CHROMIUM_USER_DATA_DIR" ]; then
|
||
# 删除 SingletonLock 文件
|
||
rm -f "$CHROMIUM_USER_DATA_DIR/SingletonLock" || true
|
||
rm -f "$CHROMIUM_USER_DATA_DIR/SingletonSocket" || true
|
||
rm -f "$CHROMIUM_USER_DATA_DIR/SingletonCookie" || true
|
||
|
||
# 清理崩溃状态文件
|
||
rm -rf "$CHROMIUM_USER_DATA_DIR/Crash Reports/pending/"* || true
|
||
rm -f "$CHROMIUM_USER_DATA_DIR/.org.chromium.Chromium."* || true
|
||
|
||
# 删除可能的临时锁文件
|
||
find "$CHROMIUM_USER_DATA_DIR" -name "*.lock" -type f -delete 2>/dev/null || true
|
||
find "$CHROMIUM_USER_DATA_DIR" -name "lockfile" -type f -delete 2>/dev/null || true
|
||
|
||
log_success "Chromium lock files cleaned from: $CHROMIUM_USER_DATA_DIR"
|
||
fi
|
||
|
||
# 5.1 清理 agent-browser 独立 profile 锁文件(如果与 MCP profile 不同)
|
||
if [ "$AGENT_BROWSER_PROFILE" != "$CHROMIUM_USER_DATA_DIR" ]; then
|
||
mkdir -p "$AGENT_BROWSER_PROFILE"
|
||
rm -f "$AGENT_BROWSER_PROFILE/SingletonLock" || true
|
||
rm -f "$AGENT_BROWSER_PROFILE/SingletonSocket" || true
|
||
rm -f "$AGENT_BROWSER_PROFILE/SingletonCookie" || true
|
||
find "$AGENT_BROWSER_PROFILE" -name "*.lock" -type f -delete 2>/dev/null || true
|
||
find "$AGENT_BROWSER_PROFILE" -name "lockfile" -type f -delete 2>/dev/null || true
|
||
log_success "agent-browser lock files cleaned from: $AGENT_BROWSER_PROFILE"
|
||
fi
|
||
|
||
# 6. 清理 /tmp 中的 Chromium 临时文件
|
||
rm -rf /tmp/.org.chromium.Chromium.* || true
|
||
rm -rf /tmp/chrome_* || true
|
||
|
||
# 7. 清理 /dev/shm 中的 Chromium 共享内存
|
||
rm -rf /dev/shm/.org.chromium.Chromium.* || true
|
||
|
||
log_success "Chromium cleanup completed (data dir: $CHROMIUM_USER_DATA_DIR)"
|
||
|
||
# 创建用户运行时目录并设置权限
|
||
USER_ID=$(id -u user)
|
||
mkdir -p /run/user/${USER_ID}
|
||
chmod 700 /run/user/${USER_ID}
|
||
chown user:user /run/user/${USER_ID}
|
||
|
||
# ========== 关键修复:设置 UTF-8 locale ==========
|
||
# 确保 locale 是 UTF-8,否则中文输入会失败
|
||
export LANG=C.UTF-8
|
||
export LC_ALL=C.UTF-8
|
||
export LC_CTYPE=C.UTF-8
|
||
|
||
# 启动 D-Bus 会话 (以 root 启动,但 HOME 设置为 /home/user)
|
||
log "Starting D-Bus session as root (HOME=/home/user)..."
|
||
HOME=/home/user dbus-launch --sh-syntax > /tmp/dbus-session-env
|
||
|
||
# 等待 D-Bus 会话文件生成(智能等待,最长 3 秒)
|
||
wait_for_file /tmp/dbus-session-env 3 || log_warn "D-Bus session file not created"
|
||
|
||
# 导出 D-Bus 会话地址供后续使用
|
||
DBUS_ADDR=""
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
DBUS_ADDR="$DBUS_SESSION_BUS_ADDRESS"
|
||
echo "D-Bus session: $DBUS_ADDR"
|
||
|
||
# ========== 关键修复:将 D-Bus 地址导出到全局环境 ==========
|
||
# 将 D-Bus 会话地址写入 /etc/environment,确保所有后续进程都能访问
|
||
echo "DBUS_SESSION_BUS_ADDRESS=\"${DBUS_ADDR}\"" >> /etc/environment
|
||
# 同时导出到当前 shell 环境
|
||
export DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}"
|
||
log_success "D-Bus address exported to global environment"
|
||
|
||
# ========== 关键修复:允许 root 访问 user 的 D-Bus socket ==========
|
||
# 修改 D-Bus socket 文件权限,允许 root 用户连接(用于 MCP chromium 中文输入)
|
||
chmod 777 /tmp/dbus-* 2>/dev/null || true
|
||
log_success "D-Bus socket permissions updated for root access"
|
||
fi
|
||
|
||
# ========== 优化:D-Bus system bus + PolicyKit 后台启动 ==========
|
||
# 这两个服务不是 VNC/XFCE 的前置依赖,串行等待会浪费 ~10 秒
|
||
# 改为后台启动,不阻塞 Xvnc → fcitx5 → xfdesktop 的关键路径
|
||
(
|
||
log "Starting D-Bus system bus..."
|
||
mkdir -p /var/run/dbus
|
||
dbus-daemon --system --fork
|
||
|
||
if wait_for_file /var/run/dbus/system_bus_socket 5; then
|
||
log_success "D-Bus system bus socket ready"
|
||
else
|
||
log_warn "D-Bus system bus socket not ready"
|
||
fi
|
||
|
||
log "Starting PolicyKit daemon..."
|
||
/usr/lib/policykit-1/polkitd --no-debug >/var/log/polkitd.log 2>&1 &
|
||
|
||
if wait_for_process "polkitd" 5; then
|
||
log_success "PolicyKit daemon started"
|
||
else
|
||
log_warn "PolicyKit daemon not started"
|
||
fi
|
||
) &
|
||
|
||
# 等待Xvnc启动(此时应该已经差不多就绪了)
|
||
log "Waiting for X11 to be ready..."
|
||
counter=0
|
||
while ! DISPLAY=:0 xdpyinfo >/dev/null 2>&1; do
|
||
sleep 0.1
|
||
let counter++
|
||
if ((counter > 100)); then
|
||
echo "Failed to start Xvnc"
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
# ========== 优化:设置初始背景色(避免纯黑死屏) ==========
|
||
# 在壁纸加载前,将 X root 窗口设置为深灰色,提供视觉反馈
|
||
DISPLAY=:0 xsetroot -solid "#1e1e1e" 2>/dev/null || true
|
||
log_success "X root window color set to dark grey"
|
||
|
||
# ========== 关键修复:手动启动 fcitx5,确保环境变量正确 ==========
|
||
# 不再依赖 XFCE autostart,直接用正确的环境变量启动 (as root, HOME=/home/user)
|
||
log "Starting fcitx5 input method (as root)..."
|
||
env \
|
||
HOME=/home/user \
|
||
DISPLAY=:0 \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
LANG=C.UTF-8 \
|
||
LC_ALL=C.UTF-8 \
|
||
LC_CTYPE=C.UTF-8 \
|
||
GTK_IM_MODULE=fcitx \
|
||
QT_IM_MODULE=fcitx \
|
||
XMODIFIERS=@im=fcitx \
|
||
INPUT_METHOD=fcitx \
|
||
fcitx5 -d --replace >/tmp/fcitx5-startup.log 2>&1 &
|
||
|
||
# 等待 fcitx5 进程启动(智能等待,最长 5 秒)
|
||
if wait_for_process "fcitx5" 5; then
|
||
log_success "fcitx5 started successfully"
|
||
else
|
||
log_warn "fcitx5 failed to start, check /tmp/fcitx5-startup.log"
|
||
fi
|
||
|
||
# 以 root 用户启动 XFCE4 会话(但 HOME 设置为 /home/user)
|
||
# 注意:使用 @im=fcitx 与系统 immodule 兼容
|
||
export DISPLAY=:0
|
||
export HOME=/home/user
|
||
export XDG_CURRENT_DESKTOP=XFCE
|
||
export XDG_SESSION_DESKTOP=xfce
|
||
export XDG_RUNTIME_DIR=/run/user/0
|
||
export GNOME_KEYRING_CONTROL=/run/user/0/keyring
|
||
export GTK_MODULES=gnome-keyring-pkcs11
|
||
export DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}"
|
||
export LANG=C.UTF-8
|
||
export LC_ALL=C.UTF-8
|
||
export LC_CTYPE=C.UTF-8
|
||
export GTK_IM_MODULE=fcitx
|
||
export QT_IM_MODULE=fcitx
|
||
export XMODIFIERS=@im=fcitx
|
||
export INPUT_METHOD=fcitx
|
||
export SDL_IM_MODULE=fcitx
|
||
export GLFW_IM_MODULE=ibus
|
||
|
||
log "Environment variables set:"
|
||
echo " HOME=$HOME"
|
||
echo " GTK_IM_MODULE=$GTK_IM_MODULE"
|
||
echo " XMODIFIERS=$XMODIFIERS"
|
||
echo " DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS"
|
||
echo " LANG=$LANG"
|
||
|
||
# 启动 gnome-keyring-daemon
|
||
gnome-keyring-daemon --start --components=secrets,ssh,pkcs11 >/dev/null 2>&1 &
|
||
|
||
# 启动 PolicyKit 认证代理
|
||
/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1 >/var/log/polkit-agent.log 2>&1 &
|
||
|
||
# 等待 gnome-keyring-daemon 启动(智能等待,最长 2 秒)
|
||
wait_for_process_pattern "gnome-keyring-daemon" 2 || true
|
||
|
||
echo 'Fcitx5 already started manually'
|
||
|
||
# ========== 优化:预启动 xfdesktop(壁纸渲染进程)==========
|
||
# xfdesktop 负责渲染桌面壁纸,默认在 xfce4-session 串行启动序列的后面
|
||
# 手动预启动可以让壁纸渲染提前开始,减少 VNC 打开时的黑屏时间
|
||
log "Pre-starting xfdesktop for faster wallpaper rendering..."
|
||
env \
|
||
DISPLAY=:0 \
|
||
HOME=/home/user \
|
||
XDG_CURRENT_DESKTOP=XFCE \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
xfdesktop &
|
||
|
||
# 智能等待 xfdesktop 进程启动(最长 5 秒,每 200ms 检测一次)
|
||
if wait_for_process "xfdesktop" 5; then
|
||
log_success "xfdesktop pre-started successfully"
|
||
else
|
||
log_warn "xfdesktop pre-start not detected, xfce4-session will start it"
|
||
fi
|
||
|
||
# 以 root 身份启动 XFCE4 会话(xfce4-session 会检测到 xfdesktop 已在运行,不会重复启动)
|
||
env \
|
||
DISPLAY=:0 \
|
||
HOME=/home/user \
|
||
XDG_CURRENT_DESKTOP=XFCE \
|
||
XDG_SESSION_DESKTOP=xfce \
|
||
XDG_RUNTIME_DIR=/run/user/0 \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
LANG=C.UTF-8 \
|
||
LC_ALL=C.UTF-8 \
|
||
LC_CTYPE=C.UTF-8 \
|
||
GTK_IM_MODULE=fcitx \
|
||
QT_IM_MODULE=fcitx \
|
||
XMODIFIERS=@im=fcitx \
|
||
INPUT_METHOD=fcitx \
|
||
xfce4-session &
|
||
|
||
log "X11 display and XFCE4 desktop started successfully (as root, HOME=/home/user)"
|
||
}
|
||
|
||
# ============================================================================
|
||
# 🎯 XFCE 壁纸设置(在 XFCE 启动后动态设置)
|
||
# XFCE 会根据显示器动态生成 xfce4-desktop.xml,需要在运行时设置壁纸
|
||
# 其他配置(screensaver, power-manager, panel)已在 /etc/xdg/xfce4 系统目录中
|
||
# ============================================================================
|
||
function apply_xfce_wallpaper() {
|
||
log "Applying XFCE wallpaper (as root)..."
|
||
|
||
# ========== 1. 等待 xfdesktop 进程启动 ==========
|
||
# xfdesktop 已在 start_display_and_desktop() 中预启动
|
||
# 这里只需要短暂等待确认它已运行(最多 30 秒,兼容慢速云服务器)
|
||
log "Waiting for xfdesktop process..."
|
||
if wait_for_process "xfdesktop" 30; then
|
||
log_success "xfdesktop process is running"
|
||
else
|
||
log_warn "xfdesktop not detected after 30s, continuing anyway (xfce4-session may start it)"
|
||
fi
|
||
|
||
# ========== 2. 等待 xfconf-query 可用 ==========
|
||
local counter=0
|
||
while ! DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -l >/dev/null 2>&1; do
|
||
sleep 1
|
||
((counter++))
|
||
if ((counter > 30)); then
|
||
log_warn " Timeout waiting for XFCE desktop xfconf, skipping wallpaper"
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
# 壁纸路径:支持通过环境变量 CUSTOM_WALLPAPER_PATH 自定义
|
||
# 如果自定义壁纸不存在,使用容器内的默认壁纸
|
||
local CUSTOM_WALLPAPER="${CUSTOM_WALLPAPER_PATH:-}"
|
||
if [ -n "$CUSTOM_WALLPAPER" ] && [ -f "$CUSTOM_WALLPAPER" ]; then
|
||
local WALLPAPER_PATH="$CUSTOM_WALLPAPER"
|
||
log "Using custom wallpaper: $WALLPAPER_PATH"
|
||
else
|
||
local WALLPAPER_PATH="/usr/share/backgrounds/xfce/wallpaper.jpeg"
|
||
if [ -n "$CUSTOM_WALLPAPER" ]; then
|
||
log_warn " Custom wallpaper not found: $CUSTOM_WALLPAPER, using default"
|
||
fi
|
||
fi
|
||
|
||
if [ ! -f "$WALLPAPER_PATH" ]; then
|
||
log_warn " Wallpaper not found: $WALLPAPER_PATH"
|
||
return 1
|
||
fi
|
||
|
||
log_success " Setting wallpaper: $WALLPAPER_PATH"
|
||
|
||
# 获取当前的 monitor 配置(XFCE 可能使用不同的名称)
|
||
# 动态获取可能会漏掉一些配置,所以最后会兜底设置常用路径
|
||
local monitors=$(DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -l 2>/dev/null | grep 'workspace0/last-image' | head -10)
|
||
|
||
if [ -n "$monitors" ]; then
|
||
# 对于每个找到的 monitor 配置设置壁纸
|
||
echo "$monitors" | while read monitor_path; do
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "$monitor_path" -s "$WALLPAPER_PATH" 2>/dev/null || true
|
||
# 同时设置 image-style (5 = 缩放)
|
||
local style_path=$(echo "$monitor_path" | sed 's/last-image/image-style/')
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "$style_path" -n -t int -s 5 2>/dev/null || true
|
||
done
|
||
fi
|
||
|
||
# 兜底:确保所有常用的 monitor 路径都设置壁纸
|
||
# 包括根级别的 monitor 配置(如 monitor0/monitor1)
|
||
# 设置所有 workspace (0-3) 的壁纸
|
||
|
||
# 1. 先设置根级别的 monitor 路径(这些优先级更高)
|
||
for monitor_path in \
|
||
"/backdrop/screen0/monitor0/last-image" \
|
||
"/backdrop/screen0/monitor1/last-image"; do
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "$monitor_path" -n -t string -s "$WALLPAPER_PATH" 2>/dev/null || true
|
||
done
|
||
|
||
# 2. 设置所有 workspace 级别的路径
|
||
for workspace in 0 1 2 3; do
|
||
for monitor_path in \
|
||
"/backdrop/screen0/monitorscreen/workspace${workspace}/last-image" \
|
||
"/backdrop/screen0/monitor0/workspace${workspace}/last-image" \
|
||
"/backdrop/screen0/monitor1/workspace${workspace}/last-image" \
|
||
"/backdrop/screen0/monitorVNC-0/workspace${workspace}/last-image"; do
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "$monitor_path" -n -t string -s "$WALLPAPER_PATH" 2>/dev/null || true
|
||
done
|
||
done
|
||
|
||
# 设置 image-path/image-show (某些 monitor 使用这种配置)
|
||
for monitor_name in monitor0 monitor1; do
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "/backdrop/screen0/${monitor_name}/image-path" -n -t string -s "$WALLPAPER_PATH" 2>/dev/null || true
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "/backdrop/screen0/${monitor_name}/image-show" -n -t bool -s true 2>/dev/null || true
|
||
done
|
||
|
||
# 设置所有找到的 image-style
|
||
for style_path in \
|
||
"/backdrop/screen0/monitorscreen/workspace0/image-style" \
|
||
"/backdrop/screen0/monitorVNC-0/workspace0/image-style" \
|
||
"/backdrop/screen0/monitorVNC-0/workspace1/image-style" \
|
||
"/backdrop/screen0/monitorVNC-0/workspace2/image-style" \
|
||
"/backdrop/screen0/monitorVNC-0/workspace3/image-style"; do
|
||
DISPLAY=:0 HOME=/home/user xfconf-query -c xfce4-desktop -p "$style_path" -n -t int -s 5 2>/dev/null || true
|
||
done
|
||
|
||
log_success "XFCE wallpaper config applied"
|
||
|
||
# ========== 3. 等待壁纸实际渲染完成 ==========
|
||
# xfdesktop 需要时间读取配置、加载图片、渲染到 X11 根窗口
|
||
# 使用 xprop 检测桌面窗口的 _XROOTPMAP_ID 属性(表示背景图已设置)
|
||
log "Waiting for wallpaper to render..."
|
||
local render_counter=0
|
||
local render_detected=false
|
||
|
||
while ((render_counter < 60)); do # 最长等待 30 秒
|
||
# 检测根窗口是否已设置背景图 (xfdesktop 设置壁纸后会更新这个属性)
|
||
if DISPLAY=:0 xprop -root _XROOTPMAP_ID 2>/dev/null | grep -q "pixmap id"; then
|
||
render_detected=true
|
||
log_success "Wallpaper rendering detected via _XROOTPMAP_ID"
|
||
break
|
||
fi
|
||
|
||
# 备选检测:检查 xfdesktop 窗口是否存在并可见
|
||
if DISPLAY=:0 xdotool search --class xfdesktop 2>/dev/null | head -1 | grep -q .; then
|
||
# xfdesktop 窗口已存在,继续轮询 _XROOTPMAP_ID 最多 2 秒,确认壁纸已渲染
|
||
local pixmap_wait=0
|
||
while ((pixmap_wait < 4)); do
|
||
if DISPLAY=:0 xprop -root _XROOTPMAP_ID 2>/dev/null | grep -q "pixmap id"; then
|
||
render_detected=true
|
||
log_success "Wallpaper rendering detected via _XROOTPMAP_ID (after xfdesktop window found)"
|
||
break 2 # 跳出两层循环
|
||
fi
|
||
sleep 0.5
|
||
((pixmap_wait++))
|
||
done
|
||
# 如果轮询后仍未检测到 pixmap,但 xfdesktop 窗口存在,也认为渲染完成
|
||
render_detected=true
|
||
log_success "Wallpaper rendering assumed complete (xfdesktop window exists)"
|
||
break
|
||
fi
|
||
|
||
sleep 0.5
|
||
((render_counter++))
|
||
done
|
||
|
||
if [ "$render_detected" = false ]; then
|
||
log_warn "Wallpaper render detection timed out, using fallback polling"
|
||
# 降级方案:继续轮询 _XROOTPMAP_ID,每 0.5 秒检测一次,最多 3 秒
|
||
local fallback_wait=0
|
||
while ((fallback_wait < 6)); do
|
||
if DISPLAY=:0 xprop -root _XROOTPMAP_ID 2>/dev/null | grep -q "pixmap id"; then
|
||
log_success "Wallpaper rendering detected via fallback polling"
|
||
break
|
||
fi
|
||
sleep 0.5
|
||
((fallback_wait++))
|
||
done
|
||
fi
|
||
|
||
log_success "XFCE wallpaper applied and rendered successfully"
|
||
|
||
# 🆕 写入壁纸就绪标记,供 VNC 就绪检查使用
|
||
echo "$(date +%s)" > /tmp/wallpaper_ready
|
||
log_success "Wallpaper ready marker written to /tmp/wallpaper_ready"
|
||
}
|
||
|
||
function check_vnc_health() {
|
||
# 检查 VNC 服务健康状态 (as root)
|
||
# 返回值:0=健康, 1=noVNC 异常(Xvnc 正常), 2=Xvnc 崩溃(需要重建整个显示栈)
|
||
if [ "$VNC_AUTO_START" = "true" ]; then
|
||
# 检查 Xvnc 进程(根基:X11 server + VNC server)
|
||
if ! pgrep -f "Xvnc" >/dev/null 2>&1; then
|
||
log_warn "Xvnc process not running (X11 display lost)"
|
||
return 2
|
||
fi
|
||
|
||
# 检查 noVNC 代理端口
|
||
if ! netstat -tuln 2>/dev/null | grep -q ":6080 "; then
|
||
log_warn "noVNC proxy not listening on port 6080"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# 重建整个显示栈:Xvnc + D-Bus session + fcitx5 + xfdesktop + xfce4-session + noVNC
|
||
# 当 Xvnc 崩溃时调用,因为 Xvnc 死亡会导致所有 X11 客户端进程一起挂掉
|
||
function restart_full_display_stack() {
|
||
log_error "Xvnc crashed, rebuilding full display stack..."
|
||
|
||
# 1. 清除所有就绪标记
|
||
rm -f /tmp/vnc_ready /tmp/novnc_port_ready /tmp/wallpaper_ready
|
||
|
||
# 2. 清理残留进程(Xvnc 死后这些都已断连,但可能变僵尸)
|
||
pkill -f "Xvnc :0" 2>/dev/null || true
|
||
pkill -f "xfce4-session" 2>/dev/null || true
|
||
pkill -f "xfdesktop" 2>/dev/null || true
|
||
pkill -f "fcitx5" 2>/dev/null || true
|
||
pkill -f "novnc_proxy" 2>/dev/null || true
|
||
pkill -f "xfce4-panel" 2>/dev/null || true
|
||
pkill -f "xfwm4" 2>/dev/null || true
|
||
pkill -f "xfsettingsd" 2>/dev/null || true
|
||
sleep 2
|
||
|
||
# 3. 清理 X11 锁文件
|
||
rm -f /tmp/.X0-lock /tmp/.X11-unix/X0
|
||
|
||
# 4. 重启 Xvnc
|
||
log "Restarting Xvnc :0 ..."
|
||
HOME=/home/user XAUTHORITY=/tmp/.Xauthority MESA_SHADER_CACHE_DIR=/tmp/mesa_shader_cache \
|
||
Xvnc :0 -geometry 1920x1080 -depth 24 -SecurityTypes None -ac -rfbport 5900 -FrameRate 20 -AlwaysShared \
|
||
>/tmp/xvnc.log 2>&1 &
|
||
|
||
# 5. 等待 X11 display 就绪
|
||
local counter=0
|
||
while ! DISPLAY=:0 xdpyinfo >/dev/null 2>&1; do
|
||
sleep 0.2
|
||
counter=$((counter + 1))
|
||
if [ $counter -ge 50 ]; then
|
||
log_error "Xvnc restart failed: X11 display not ready after 10s"
|
||
return 1
|
||
fi
|
||
done
|
||
log_success "Xvnc restarted, X11 display :0 ready"
|
||
|
||
# 6. 设置背景色(避免纯黑)
|
||
DISPLAY=:0 xsetroot -solid "#1e1e1e" 2>/dev/null || true
|
||
|
||
# 7. 获取 D-Bus session 地址
|
||
local DBUS_ADDR=""
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
DBUS_ADDR="$DBUS_SESSION_BUS_ADDRESS"
|
||
fi
|
||
|
||
# 8. 重启 fcitx5 输入法
|
||
env HOME=/home/user DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
LANG=C.UTF-8 LC_ALL=C.UTF-8 LC_CTYPE=C.UTF-8 \
|
||
GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx INPUT_METHOD=fcitx \
|
||
fcitx5 -d --replace >/tmp/fcitx5-startup.log 2>&1 &
|
||
|
||
# 9. 重启 xfdesktop(壁纸渲染)
|
||
env DISPLAY=:0 HOME=/home/user XDG_CURRENT_DESKTOP=XFCE \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
xfdesktop &
|
||
|
||
# 10. 重启 xfce4-session(桌面会话管理器)
|
||
env DISPLAY=:0 HOME=/home/user XDG_CURRENT_DESKTOP=XFCE XDG_SESSION_DESKTOP=xfce \
|
||
XDG_RUNTIME_DIR=/run/user/0 DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
LANG=C.UTF-8 LC_ALL=C.UTF-8 LC_CTYPE=C.UTF-8 \
|
||
GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx INPUT_METHOD=fcitx \
|
||
xfce4-session &
|
||
|
||
# 11. 等待 xfdesktop 启动
|
||
if wait_for_process "xfdesktop" 10; then
|
||
log_success "xfdesktop restarted"
|
||
else
|
||
log_warn "xfdesktop not detected after restart"
|
||
fi
|
||
|
||
# 12. 重启 noVNC 代理
|
||
nohup /opt/noVNC/utils/novnc_proxy \
|
||
--vnc localhost:5900 --listen 6080 --web /opt/noVNC \
|
||
--heartbeat 30 \
|
||
> /tmp/novnc.log 2>&1 &
|
||
|
||
# 13. 验证 noVNC 端口 + WebSocket
|
||
if wait_for_port localhost 6080 20 && wait_for_novnc_ready 10; then
|
||
echo "$(date +%s)" > /tmp/novnc_port_ready
|
||
log_success "noVNC proxy restarted and WebSocket verified"
|
||
else
|
||
log_error "noVNC restart failed after Xvnc recovery"
|
||
return 1
|
||
fi
|
||
|
||
# 14. 重新应用壁纸(后台,不阻塞)
|
||
(
|
||
apply_xfce_wallpaper
|
||
) &
|
||
|
||
# 15. 重启 VNC 就绪标记轮询(后台)
|
||
# novnc_port_ready 已写入,等 wallpaper_ready + xfdesktop 进程就绪后写 vnc_ready
|
||
(
|
||
wait_and_write_vnc_ready_marker
|
||
) &
|
||
|
||
log_success "Full display stack rebuilt successfully"
|
||
return 0
|
||
}
|
||
|
||
function check_mcp_proxy_health() {
|
||
# 检查 MCP Proxy 服务健康状态
|
||
# MCP Proxy 运行在 127.0.0.1:18099,提供 chrome-devtools MCP 服务
|
||
# 使用 mcp-proxy health 命令进行健康检查,支持 Streamable HTTP 协议
|
||
|
||
local MCP_PROXY_PORT=18099
|
||
|
||
# 1. 快速检查 mcp-proxy 进程是否存在
|
||
if ! pgrep -f "mcp-proxy proxy" >/dev/null 2>&1; then
|
||
log_warn "MCP Proxy process not running"
|
||
return 1
|
||
fi
|
||
|
||
# 2. 使用 mcp-proxy health 命令检查服务健康状态
|
||
# -q: 静默模式,只返回退出码(0=健康,1=不健康)
|
||
# --timeout 5: 超时 5 秒
|
||
if mcp-proxy health "http://127.0.0.1:${MCP_PROXY_PORT}" -q --timeout 5; then
|
||
# 健康检查成功,不输出日志以减少日志量
|
||
return 0
|
||
else
|
||
log_warn "MCP Proxy health check failed on port ${MCP_PROXY_PORT}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
function restart_mcp_proxy() {
|
||
# 重启 MCP Proxy 服务
|
||
log "Restarting MCP Proxy service..."
|
||
|
||
local MCP_LOG_DIR="${CONTAINER_LOGS_DIR:-/app/container-logs}/mcp"
|
||
local MCP_CONFIG_FILE="/etc/mcp/mcp-proxy-config.json"
|
||
|
||
# 确保日志目录存在
|
||
mkdir -p "$MCP_LOG_DIR" 2>/dev/null || true
|
||
|
||
# ========== 1. 停止现有 MCP Proxy 进程 ==========
|
||
log " Stopping existing MCP Proxy processes..."
|
||
pkill -f "mcp-proxy proxy" || true
|
||
sleep 1
|
||
|
||
# 强制杀死残留进程
|
||
pkill -9 -f "mcp-proxy proxy" 2>/dev/null || true
|
||
sleep 1
|
||
|
||
# ========== 2. 🔥 关键修复:清理所有 chrome-headless 子进程(解决僵尸进程问题)==========
|
||
log " Cleaning up chrome-headless and chromium processes..."
|
||
|
||
# 2.1 清理 chrome-headless 进程(包括僵尸进程)
|
||
local chrome_count=$(ps aux | grep -E '[c]hrome-headless' | wc -l)
|
||
if [ "$chrome_count" -gt 0 ]; then
|
||
log " Found $chrome_count chrome-headless processes, terminating..."
|
||
# 优雅终止
|
||
pkill -TERM -f "chrome-headless" 2>/dev/null || true
|
||
sleep 2
|
||
# 强制杀死残留进程
|
||
pkill -9 -f "chrome-headless" 2>/dev/null || true
|
||
fi
|
||
|
||
# 2.2 清理 chromium-for-mcp 进程
|
||
local chromium_count=$(ps aux | grep -E '[c]hromium-for-mcp' | wc -l)
|
||
if [ "$chromium_count" -gt 0 ]; then
|
||
log " Found $chromium_count chromium-for-mcp processes, terminating..."
|
||
pkill -TERM -f "chromium-for-mcp" 2>/dev/null || true
|
||
sleep 2
|
||
pkill -9 -f "chromium-for-mcp" 2>/dev/null || true
|
||
fi
|
||
|
||
# 2.3 清理所有 chromium 相关的僵尸进程
|
||
# 🔧 修复:使用更兼容的方式,避免 xargs -r
|
||
local zombie_pids=$(ps aux | grep -E '[c]hrome.*<defunct>|[c]hromium.*<defunct>' | awk '{print $2}')
|
||
if [ -n "$zombie_pids" ]; then
|
||
echo "$zombie_pids" | xargs kill -9 2>/dev/null || true
|
||
fi
|
||
|
||
# 2.4 清理 chrome-devtools-mcp 进程
|
||
pkill -9 -f "chrome-devtools-mcp" 2>/dev/null || true
|
||
|
||
# 2.5 清理 Chromium 相关的锁文件和临时文件
|
||
local CHROMIUM_DATA_DIR="${CHROMIUM_USER_DATA_DIR:-/home/user/.config/chromium}"
|
||
if [ -d "$CHROMIUM_DATA_DIR" ]; then
|
||
log " Cleaning Chromium lock files in $CHROMIUM_DATA_DIR..."
|
||
# 删除 SingletonLock 文件
|
||
rm -f "$CHROMIUM_DATA_DIR/SingletonLock" 2>/dev/null || true
|
||
rm -f "$CHROMIUM_DATA_DIR/SingletonSocket" 2>/dev/null || true
|
||
rm -f "$CHROMIUM_DATA_DIR/SingletonCookie" 2>/dev/null || true
|
||
# 清理所有锁文件
|
||
find "$CHROMIUM_DATA_DIR" -name "*.lock" -type f -delete 2>/dev/null || true
|
||
find "$CHROMIUM_DATA_DIR" -name "lockfile" -type f -delete 2>/dev/null || true
|
||
fi
|
||
|
||
# 2.6 清理 /tmp 中的 Chromium 临时文件
|
||
rm -rf /tmp/.org.chromium.Chromium.* 2>/dev/null || true
|
||
rm -rf /tmp/chrome_* 2>/dev/null || true
|
||
|
||
# 2.7 清理 /dev/shm 中的 Chromium 共享内存
|
||
rm -rf /dev/shm/.org.chromium.Chromium.* 2>/dev/null || true
|
||
|
||
sleep 2
|
||
|
||
# 2.8 统计清理后的进程数
|
||
local remaining_chrome=$(ps aux | grep -E '[c]hrome-headless|[c]hromium-for-mcp' | grep -v grep | wc -l)
|
||
if [ "$remaining_chrome" -gt 0 ]; then
|
||
log_warn " ⚠️ Still have $remaining_chrome chrome processes (may be from other containers)"
|
||
else
|
||
log_success " ✅ All chrome-headless and chromium processes cleaned"
|
||
fi
|
||
|
||
# ========== 3. 检查配置文件 ==========
|
||
if [ ! -f "$MCP_CONFIG_FILE" ]; then
|
||
log_warn "MCP config file not found: $MCP_CONFIG_FILE, cannot restart"
|
||
return 1
|
||
fi
|
||
|
||
# ========== 4. 获取 D-Bus 地址 ==========
|
||
local DBUS_ADDR=""
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
DBUS_ADDR="$DBUS_SESSION_BUS_ADDRESS"
|
||
fi
|
||
|
||
# ========== 5. 🔥 关键修复:使用进程组启动 mcp-proxy proxy 服务 ==========
|
||
log " Starting mcp-proxy proxy with process group management..."
|
||
|
||
# 使用 setsid 创建新的会话和进程组,便于后续清理
|
||
env \
|
||
HOME=/home/user \
|
||
DISPLAY=:0 \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
CHROMIUM_USER_DATA_DIR=/home/user/.config/chromium \
|
||
GTK_IM_MODULE=fcitx \
|
||
QT_IM_MODULE=fcitx \
|
||
XMODIFIERS=@im=fcitx \
|
||
INPUT_METHOD=fcitx \
|
||
LANG=C.UTF-8 \
|
||
LC_ALL=C.UTF-8 \
|
||
LC_CTYPE=C.UTF-8 \
|
||
PATH="/usr/local/bin:/opt/cargo/bin:$PATH" \
|
||
setsid bash -c "
|
||
exec mcp-proxy proxy --port 18099 --host 127.0.0.1 --config-file '$MCP_CONFIG_FILE' --log-dir /app/container-logs -v \
|
||
> '$MCP_LOG_DIR/mcp-proxy.log' 2>&1
|
||
" &
|
||
|
||
local MCP_PID=$!
|
||
# 🔧 修复:使用更兼容的方式获取 PGID
|
||
local MCP_PGID=$(ps -p $MCP_PID -o pgid= 2>/dev/null | tr -d '[:space:]')
|
||
|
||
# 保存 PID 和 PGID 到文件,便于后续清理
|
||
# 🔧 修复:添加错误处理,回退到持久化目录
|
||
if ! echo "$MCP_PID" > /var/run/mcp-proxy.pid 2>/dev/null; then
|
||
mkdir -p /app/container-logs
|
||
echo "$MCP_PID" > /app/container-logs/mcp-proxy.pid
|
||
fi
|
||
|
||
if ! echo "$MCP_PGID" > /var/run/mcp-proxy.pgid 2>/dev/null; then
|
||
mkdir -p /app/container-logs
|
||
echo "$MCP_PGID" > /app/container-logs/mcp-proxy.pgid
|
||
fi
|
||
|
||
# ========== 6. 等待端口就绪(最长 15 秒)==========
|
||
if wait_for_port 127.0.0.1 18099 15 && kill -0 $MCP_PID 2>/dev/null; then
|
||
log_success "MCP Proxy restarted successfully (PID: $MCP_PID, PGID: $MCP_PGID)"
|
||
return 0
|
||
else
|
||
log_warn "MCP Proxy restart failed, check log: $MCP_LOG_DIR/mcp-proxy.log"
|
||
tail -20 "$MCP_LOG_DIR/mcp-proxy.log" 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ============================================================================
|
||
# 🎯 VNC 就绪标记轮询任务
|
||
# 同时等待 noVNC 端口和桌面壁纸都就绪后,才写入最终的 VNC 就绪标记
|
||
# 这样前端打开 VNC 远程桌面时,桌面一定是正常显示的
|
||
# ============================================================================
|
||
function wait_and_write_vnc_ready_marker() {
|
||
log "Starting VNC ready marker polling task (no timeout, will wait indefinitely)..."
|
||
|
||
local interval=1 # 每秒检查一次
|
||
local elapsed=0
|
||
|
||
while true; do
|
||
local novnc_ready=false
|
||
local wallpaper_ready=false
|
||
local desktop_ready=false
|
||
|
||
# 检查 noVNC 端口就绪标记
|
||
if [ -f /tmp/novnc_port_ready ]; then
|
||
novnc_ready=true
|
||
fi
|
||
|
||
# 检查壁纸就绪标记
|
||
if [ -f /tmp/wallpaper_ready ]; then
|
||
wallpaper_ready=true
|
||
fi
|
||
|
||
# 检查 xfdesktop 桌面进程是否在运行
|
||
# 确保桌面环境已加载,避免用户连上看到灰屏
|
||
if pgrep -f "xfdesktop" >/dev/null 2>&1; then
|
||
desktop_ready=true
|
||
fi
|
||
|
||
# 三者都就绪时,写入最终的 VNC 就绪标记
|
||
if [ "$novnc_ready" = true ] && [ "$wallpaper_ready" = true ] && [ "$desktop_ready" = true ]; then
|
||
echo "$(date +%s)" > /tmp/vnc_ready
|
||
log_success "VNC ready marker written to /tmp/vnc_ready (noVNC + wallpaper + desktop all ready, took ${elapsed}s)"
|
||
log_success " noVNC port ready at: $(cat /tmp/novnc_port_ready)"
|
||
log_success " Wallpaper ready at: $(cat /tmp/wallpaper_ready)"
|
||
return 0
|
||
fi
|
||
|
||
# 日志输出当前状态(每 30 秒输出一次,减少日志量)
|
||
if ((elapsed % 30 == 0)) && ((elapsed > 0)); then
|
||
log "Waiting for VNC ready... noVNC=$novnc_ready, wallpaper=$wallpaper_ready, desktop=$desktop_ready (${elapsed}s elapsed)"
|
||
fi
|
||
|
||
sleep $interval
|
||
((elapsed += interval))
|
||
done
|
||
}
|
||
|
||
# Jupyter server function removed
|
||
|
||
# ============================================================================
|
||
# 🔊 音频流服务 (pcmflux)
|
||
# 使用 PulseAudio 虚拟声卡捕获音频,通过 WebSocket 流到浏览器
|
||
# ============================================================================
|
||
function start_audio_services() {
|
||
log "Starting audio streaming services (pcmflux)..."
|
||
|
||
# 1. 确保 PulseAudio 目录存在且有正确权限
|
||
mkdir -p /home/user/.config/pulse
|
||
mkdir -p /var/run/pulse
|
||
chmod 755 /home/user/.config/pulse
|
||
chmod 777 /var/run/pulse
|
||
|
||
# 2. 创建 PulseAudio 客户端配置文件(允许 root 连接)
|
||
cat > /home/user/.config/pulse/client.conf <<'EOF'
|
||
# 允许 root 用户连接
|
||
allow-pubkey-authentication=no
|
||
default-server=unix:/var/run/pulse/native
|
||
EOF
|
||
|
||
# 3. 创建 PulseAudio 守护进程配置(禁用自动启动锁)
|
||
cat > /home/user/.config/pulse/daemon.conf <<'EOF'
|
||
# 禁用自动启动锁
|
||
autospawn = no
|
||
exit-idle-time = -1
|
||
log-level = warning
|
||
EOF
|
||
|
||
# 4. 修复 /home/user 目录权限(如果被挂载覆盖)
|
||
chown -R user:user /home/user/.config/pulse 2>/dev/null || true
|
||
|
||
# 5. 使用 --system 模式启动 PulseAudio(容器环境)
|
||
# --disallow-exit: 防止 PulseAudio 自动退出
|
||
# --disable-shm: 禁用共享内存(容器环境兼容性)
|
||
log " Starting PulseAudio in system mode..."
|
||
pulseaudio --system \
|
||
--disallow-exit \
|
||
--disable-shm \
|
||
--no-cpu-limit \
|
||
--log-level=warning \
|
||
--daemonize=no \
|
||
2>/tmp/pulseaudio.log &
|
||
|
||
# 等待 PulseAudio 进程启动
|
||
if wait_for_process "pulseaudio" 5; then
|
||
log_success " PulseAudio daemon started (system mode)"
|
||
else
|
||
log_warn " PulseAudio failed to start, checking log..."
|
||
cat /tmp/pulseaudio.log 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
|
||
# 6. 等待 PulseAudio socket 就绪
|
||
local pulse_socket="/var/run/pulse/native"
|
||
local counter=0
|
||
while [ ! -S "$pulse_socket" ]; do
|
||
sleep 0.2
|
||
let counter++
|
||
if ((counter > 25)); then
|
||
log_warn " PulseAudio socket not ready: $pulse_socket"
|
||
return 1
|
||
fi
|
||
done
|
||
log_success " PulseAudio socket ready: $pulse_socket"
|
||
|
||
# 7. 设置 PULSE_SERVER 环境变量
|
||
export PULSE_SERVER="unix:/var/run/pulse/native"
|
||
echo "export PULSE_SERVER='unix:/var/run/pulse/native'" >> /etc/profile.d/pulse-env.sh
|
||
|
||
# 8. 创建虚拟声卡
|
||
log " Creating virtual audio sink..."
|
||
if pactl load-module module-null-sink sink_name=virtual_speaker \
|
||
sink_properties=device.description="Virtual_Speaker" 2>/dev/null; then
|
||
log_success " Virtual speaker sink created"
|
||
else
|
||
log_warn " Failed to create virtual speaker sink"
|
||
return 1
|
||
fi
|
||
|
||
# 9. 设置虚拟声卡为默认输出
|
||
pactl set-default-sink virtual_speaker 2>/dev/null || true
|
||
|
||
# 10. 启动 pcmflux 音频流服务
|
||
log " Starting pcmflux audio streaming service..."
|
||
export AUDIO_DEVICE="virtual_speaker.monitor"
|
||
export AUDIO_HTTP_PORT=6090
|
||
export AUDIO_WS_PORT=6089
|
||
|
||
nohup python3 /usr/local/bin/audio_server.py > /tmp/audio_server.log 2>&1 &
|
||
|
||
if wait_for_process_pattern "audio_server.py" 3 && wait_for_port localhost 6090 3; then
|
||
log_success " pcmflux audio server started"
|
||
log_success " Audio HTTP: http://localhost:6090"
|
||
log_success " Audio WebSocket: ws://localhost:6089"
|
||
else
|
||
log_warn " pcmflux audio server failed to start"
|
||
cat /tmp/audio_server.log 2>/dev/null | tail -20 || true
|
||
return 1
|
||
fi
|
||
|
||
log_success "Audio streaming services initialized"
|
||
return 0
|
||
}
|
||
|
||
# ============================================================================
|
||
# 🔌 MCP Proxy 服务 (chrome-devtools-mcp 共享代理)
|
||
# 将 stdio 协议的 MCP 服务代理为 HTTP 服务,供多个 agent 复用
|
||
# ============================================================================
|
||
function start_mcp_proxy_services() {
|
||
log "Starting MCP Proxy services (chrome-devtools shared)..."
|
||
|
||
# 创建 MCP 日志目录(持久化到挂载的 /app/container-logs)
|
||
local MCP_LOG_DIR="${CONTAINER_LOGS_DIR:-/app/container-logs}/mcp"
|
||
mkdir -p "$MCP_LOG_DIR"
|
||
chmod 755 "$MCP_LOG_DIR"
|
||
log_success " MCP log directory: $MCP_LOG_DIR"
|
||
|
||
# MCP 配置文件路径(由 Dockerfile 复制到 /etc/mcp)
|
||
local MCP_CONFIG_FILE="/etc/mcp/mcp-proxy-config.json"
|
||
if [ ! -f "$MCP_CONFIG_FILE" ]; then
|
||
log_warn " MCP config file not found: $MCP_CONFIG_FILE"
|
||
log_warn " MCP Proxy services will not start"
|
||
return 1
|
||
fi
|
||
log_success " MCP config file: $MCP_CONFIG_FILE"
|
||
|
||
# 获取 D-Bus 地址
|
||
local DBUS_ADDR=""
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
DBUS_ADDR="$DBUS_SESSION_BUS_ADDRESS"
|
||
fi
|
||
|
||
# ========== 🔥 关键修复:使用进程组启动 mcp-proxy proxy 服务 ==========
|
||
# 需要传递正确的环境变量(DISPLAY, D-Bus, 输入法等)
|
||
echo " Starting mcp-proxy proxy on port 18099 (with process group)..."
|
||
|
||
# 使用 setsid 创建新的会话和进程组,便于后续清理所有子进程
|
||
env \
|
||
HOME=/home/user \
|
||
DISPLAY=:0 \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
CHROMIUM_USER_DATA_DIR=/home/user/.config/chromium \
|
||
GTK_IM_MODULE=fcitx \
|
||
QT_IM_MODULE=fcitx \
|
||
XMODIFIERS=@im=fcitx \
|
||
INPUT_METHOD=fcitx \
|
||
LANG=C.UTF-8 \
|
||
LC_ALL=C.UTF-8 \
|
||
LC_CTYPE=C.UTF-8 \
|
||
PATH="/usr/local/bin:/opt/cargo/bin:$PATH" \
|
||
setsid bash -c "
|
||
exec mcp-proxy proxy --port 18099 --host 127.0.0.1 --config-file '$MCP_CONFIG_FILE' --log-dir /app/container-logs -v \
|
||
> '$MCP_LOG_DIR/mcp-proxy.log' 2>&1
|
||
" &
|
||
|
||
local MCP_PID=$!
|
||
# 🔧 修复:使用更兼容的方式获取 PGID
|
||
local MCP_PGID=$(ps -p $MCP_PID -o pgid= 2>/dev/null | tr -d '[:space:]')
|
||
|
||
# 保存 PID 和 PGID 到文件,便于后续清理
|
||
# 🔧 修复:添加错误处理,回退到持久化目录
|
||
if ! echo "$MCP_PID" > /var/run/mcp-proxy.pid 2>/dev/null; then
|
||
mkdir -p /app/container-logs
|
||
echo "$MCP_PID" > /app/container-logs/mcp-proxy.pid
|
||
fi
|
||
|
||
if ! echo "$MCP_PGID" > /var/run/mcp-proxy.pgid 2>/dev/null; then
|
||
mkdir -p /app/container-logs
|
||
echo "$MCP_PGID" > /app/container-logs/mcp-proxy.pgid
|
||
fi
|
||
|
||
# 等待 MCP Proxy 端口就绪(智能等待,最长 10 秒)
|
||
if wait_for_port 127.0.0.1 18099 10 && kill -0 $MCP_PID 2>/dev/null; then
|
||
log_success " MCP Proxy started (PID: $MCP_PID, PGID: $MCP_PGID)"
|
||
log_success " MCP Proxy URL: http://127.0.0.1:18099"
|
||
log_success " Agent 可使用: mcp-proxy convert http://127.0.0.1:18099"
|
||
else
|
||
log_warn " MCP Proxy failed to start, check log: $MCP_LOG_DIR/mcp-proxy.log"
|
||
cat "$MCP_LOG_DIR/mcp-proxy.log" 2>/dev/null | tail -20 || true
|
||
fi
|
||
|
||
log_success "MCP Proxy services initialized"
|
||
}
|
||
|
||
# ============================================================================
|
||
# ⌨️ IME 本地输入法透传服务
|
||
# 允许用户使用宿主机的输入法(如搜狗输入法)直接输入到远程桌面
|
||
# ============================================================================
|
||
function start_ime_services() {
|
||
log "Starting IME passthrough services..."
|
||
|
||
# 检查是否启用(可通过环境变量禁用)
|
||
if [ "${ENABLE_IME_PASSTHROUGH:-true}" = "false" ]; then
|
||
log_warn " IME passthrough is disabled (ENABLE_IME_PASSTHROUGH=false)"
|
||
return 0
|
||
fi
|
||
|
||
# 创建 IME 日志目录(持久化到挂载的 /app/container-logs)
|
||
local IME_LOG_DIR="${CONTAINER_LOGS_DIR:-/app/container-logs}/ime"
|
||
mkdir -p "$IME_LOG_DIR"
|
||
chmod 755 "$IME_LOG_DIR"
|
||
log_success " IME log directory: $IME_LOG_DIR"
|
||
|
||
# 检查 IME 服务脚本是否存在
|
||
local IME_SCRIPT="/usr/local/bin/ime_server.py"
|
||
if [ ! -f "$IME_SCRIPT" ]; then
|
||
log_warn " IME server script not found: $IME_SCRIPT"
|
||
log_warn " IME passthrough services will not start"
|
||
return 1
|
||
fi
|
||
|
||
# 获取 D-Bus 地址
|
||
local DBUS_ADDR=""
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
DBUS_ADDR="$DBUS_SESSION_BUS_ADDRESS"
|
||
fi
|
||
|
||
# 启动 IME 服务
|
||
echo " Starting IME server on port 6091..."
|
||
env \
|
||
HOME=/home/user \
|
||
DISPLAY=:0 \
|
||
DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDR}" \
|
||
IME_PORT=6091 \
|
||
IME_HOST=0.0.0.0 \
|
||
nohup python3 "$IME_SCRIPT" \
|
||
> "$IME_LOG_DIR/ime_server.log" 2>&1 &
|
||
|
||
local IME_PID=$!
|
||
|
||
# 等待 IME 服务端口就绪(智能等待,最长 5 秒)
|
||
if wait_for_port 127.0.0.1 6091 5 && kill -0 $IME_PID 2>/dev/null; then
|
||
log_success " IME server started (PID: $IME_PID)"
|
||
log_success " IME WebSocket: ws://0.0.0.0:6091"
|
||
log_success " 用户可使用宿主机输入法输入到远程桌面"
|
||
else
|
||
log_warn " IME server failed to start, check log: $IME_LOG_DIR/ime_server.log"
|
||
cat "$IME_LOG_DIR/ime_server.log" 2>/dev/null | tail -20 || true
|
||
fi
|
||
|
||
log_success "IME passthrough services initialized"
|
||
}
|
||
# 设置VNC自动启动标志
|
||
export VNC_AUTO_START=true
|
||
|
||
# ========== 关键:在启动 X11 之前初始化用户主目录 ==========
|
||
# 从骨架目录恢复配置(解决挂载空目录导致的花屏和图标消失)
|
||
initialize_user_home
|
||
|
||
# ========== MCP Proxy 服务在 X11 就绪后启动 ==========
|
||
# 注意:chrome-devtools-mcp 需要 X11 来启动 Chromium 浏览器
|
||
# 因此必须等待 Xvnc 启动后才能启动 MCP Proxy
|
||
# MCP Proxy 的启动已移动到下方的 VNC 后台任务中(X11 就绪后)
|
||
|
||
# 首先启动显示服务和桌面环境
|
||
start_display_and_desktop &
|
||
|
||
# 设置全局DISPLAY环境变量
|
||
export DISPLAY=:0
|
||
echo "DISPLAY=:0" >> /etc/environment
|
||
|
||
# envd 服务已删除 - 不再启动环境守护进程
|
||
|
||
# Jupyter services removed
|
||
|
||
# 启动 VNC 服务(在后台运行,等待X11就绪)
|
||
log "Starting VNC services in background (as root)..."
|
||
log "VNC will be available at: http://localhost:6080/vnc.html?autoconnect=true&resize=scale"
|
||
(
|
||
# 等待X11服务就绪
|
||
counter=0
|
||
while ! DISPLAY=:0 xdpyinfo >/dev/null 2>&1; do
|
||
sleep 1
|
||
let counter++
|
||
if ((counter > 60)); then
|
||
echo "Timeout waiting for X11, VNC services will not start"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
log "X11 is ready, starting services in parallel..."
|
||
|
||
# ========== 并行启动所有依赖 X11 的服务 ==========
|
||
# 这些服务互不依赖,可以同时启动以缩短整体启动时间
|
||
|
||
# 1. VNC 服务(后台)
|
||
(
|
||
if start_vnc_services; then
|
||
log_success "VNC services started successfully!"
|
||
log_success "VNC URL: http://localhost:6080/vnc.html?autoconnect=true&resize=scale"
|
||
log_success "Direct VNC port: 5900"
|
||
else
|
||
log_error "VNC services failed to start, /tmp/novnc_port_ready will NOT be written"
|
||
fi
|
||
) &
|
||
vnc_pid=$!
|
||
|
||
# 2. MCP Proxy 服务(后台)- 需要 X11 来启动 Chromium
|
||
(
|
||
log "Starting MCP Proxy services..."
|
||
start_mcp_proxy_services
|
||
) &
|
||
mcp_pid=$!
|
||
|
||
# 3. 音频流服务 (pcmflux)(后台)
|
||
(
|
||
start_audio_services
|
||
) &
|
||
audio_pid=$!
|
||
|
||
# 4. IME 本地输入法透传服务(后台)
|
||
(
|
||
start_ime_services
|
||
) &
|
||
ime_pid=$!
|
||
|
||
# 5. 应用 XFCE 壁纸(后台)
|
||
(
|
||
apply_xfce_wallpaper
|
||
) &
|
||
wallpaper_pid=$!
|
||
|
||
# 等待所有并行服务启动完成
|
||
log "Waiting for all services to start..."
|
||
wait $vnc_pid $mcp_pid $audio_pid $ime_pid $wallpaper_pid 2>/dev/null || true
|
||
log_success "All X11-dependent services started!"
|
||
|
||
# 🆕 启动 VNC 就绪标记轮询任务(后台)
|
||
# 等待 noVNC 端口和壁纸都就绪后,才写入最终的 /tmp/vnc_ready 标记
|
||
(
|
||
wait_and_write_vnc_ready_marker
|
||
) &
|
||
vnc_ready_marker_pid=$!
|
||
log "VNC ready marker polling task started (pid: $vnc_ready_marker_pid)"
|
||
|
||
# VNC 和 MCP Proxy 服务监控循环 (as root)
|
||
while true; do
|
||
sleep 30
|
||
|
||
# 检查 VNC 服务健康状态
|
||
# 返回值: 0=健康, 1=noVNC 异常, 2=Xvnc 崩溃
|
||
# 注意:此处不能用 local,因为当前在子 shell 中而非函数内
|
||
check_vnc_health
|
||
vnc_status=$?
|
||
|
||
if [ $vnc_status -eq 2 ]; then
|
||
# Xvnc 崩溃:整个显示栈需要重建
|
||
# Xvnc 是 X11 server + VNC server,它死了 = display :0 消失
|
||
# 所有连接到 :0 的进程(xfdesktop, xfce4-session, fcitx5 等)全部跟着死
|
||
# 只重启 noVNC 没用,因为 noVNC 连不上已死的 5900 端口
|
||
restart_full_display_stack || log_error "Full display stack restart failed"
|
||
|
||
elif [ $vnc_status -eq 1 ]; then
|
||
# 仅 noVNC 代理异常,Xvnc 仍在运行
|
||
log_warn "noVNC proxy is down, restarting..."
|
||
rm -f /tmp/vnc_ready /tmp/novnc_port_ready
|
||
|
||
pkill -f novnc_proxy || true
|
||
sleep 1
|
||
|
||
nohup /opt/noVNC/utils/novnc_proxy \
|
||
--vnc localhost:5900 \
|
||
--listen 6080 \
|
||
--web /opt/noVNC \
|
||
--heartbeat 30 \
|
||
> /tmp/novnc.log 2>&1 &
|
||
|
||
# 等待端口 + WebSocket 就绪后,重新写入标记
|
||
if wait_for_port localhost 6080 20 && wait_for_novnc_ready 10; then
|
||
echo "$(date +%s)" > /tmp/novnc_port_ready
|
||
# Xvnc 仍在运行,桌面进程应该也还在
|
||
if [ -f /tmp/wallpaper_ready ] && pgrep -f "xfdesktop" >/dev/null 2>&1; then
|
||
echo "$(date +%s)" > /tmp/vnc_ready
|
||
log_success "noVNC proxy restarted and verified successfully"
|
||
else
|
||
log_warn "noVNC restarted but desktop not ready, waiting for marker task"
|
||
fi
|
||
else
|
||
log_error "noVNC restart failed: WebSocket not ready"
|
||
fi
|
||
fi
|
||
|
||
# ========== MCP Proxy 健康检查(使用 mcp-proxy health 工具)==========
|
||
# 使用 mcp-proxy health 命令进行健康检查,正确处理 Streamable HTTP 协议
|
||
if ! check_mcp_proxy_health; then
|
||
echo "MCP Proxy 服务异常,正在重启..."
|
||
if ! restart_mcp_proxy; then
|
||
# 首次重启失败,等待 5 秒后重试一次
|
||
sleep 5
|
||
restart_mcp_proxy || log_warn "MCP Proxy restart failed after retry"
|
||
fi
|
||
fi
|
||
|
||
# 🔥 注意:僵尸进程回收已由 agent_runner 的 process_reaper 模块处理
|
||
# agent_runner 作为 PID 1 会自动回收所有孤儿进程,无需脚本额外清理
|
||
done
|
||
) &
|
||
|
||
# 启动 agent_runner 服务,支持从环境变量读取端口
|
||
log "Starting agent_runner service on port ${PORT:-8086}..."
|
||
|
||
# 🔧 如果启用 eBPF 调试模式,启动自动火焰图生成
|
||
if [ "${ENABLE_EBPF_AUTO_FLAMEGRAPH:-false}" = "true" ]; then
|
||
log "🔧 启动 eBPF 自动火焰图生成..."
|
||
/usr/local/bin/ebpf-tools/auto-flamegraph.sh start &
|
||
AUTO_FLAME_PID=$!
|
||
log "✅ eBPF 自动火焰图生成已启动 (PID: $AUTO_FLAME_PID)"
|
||
log "📋 火焰图将每 ${GENERATE_INTERVAL:-60} 秒生成一次"
|
||
log "💡 火焰图保存到: ${DIAG_OUTPUT_DIR:-/app/container-logs/diag}/flamegraph-*.svg"
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 📊 Grafana Alloy - 持续性能数据采集(替代已废弃的 Pyroscope Agent)
|
||
# 默认禁用,通过环境变量 ENABLE_ALLOY=true 启用
|
||
# ============================================================================
|
||
if [ "${ENABLE_ALLOY:-false}" = "true" ]; then
|
||
log "🔧 启动 Grafana Alloy (eBPF Profiling)..."
|
||
|
||
# Pyroscope Server 地址
|
||
PYROSCOPE_URL="${PYROSCOPE_URL:-http://pyroscope:4040}"
|
||
|
||
# 设置环境变量(用于 Alloy 配置文件)
|
||
export PYROSCOPE_URL
|
||
export PROJECT_ID="${PROJECT_ID:-default}"
|
||
export ENV="${ENV:-dev}"
|
||
export HOSTNAME="${HOSTNAME:-$(hostname)}"
|
||
|
||
# 显示环境变量(用于调试)
|
||
log " 环境变量:"
|
||
log " PYROSCOPE_URL=$PYROSCOPE_URL"
|
||
log " PROJECT_ID=$PROJECT_ID"
|
||
log " ENV=$ENV"
|
||
log " HOSTNAME=$HOSTNAME"
|
||
|
||
# 等待 Pyroscope Server 就绪
|
||
log " 等待 Pyroscope Server 就绪: $PYROSCOPE_URL"
|
||
local pyro_ready=false
|
||
local counter=0
|
||
while [ $counter -lt 60 ]; do
|
||
if curl -s -o /dev/null -w "%{http_code}" "$PYROSCOPE_URL" 2>/dev/null | grep -q "200\|404"; then
|
||
pyro_ready=true
|
||
break
|
||
fi
|
||
sleep 0.5
|
||
counter=$((counter + 1))
|
||
done
|
||
|
||
if [ "$pyro_ready" = false ]; then
|
||
log_warn " Pyroscope Server 未就绪,跳过 Alloy 启动"
|
||
else
|
||
log_success " Pyroscope Server 已就绪"
|
||
|
||
# 创建日志目录
|
||
mkdir -p "${CONTAINER_LOGS_DIR:-/app/container-logs}/diag"
|
||
|
||
# 检查 Alloy 是否安装
|
||
if ! command -v alloy >/dev/null 2>&1; then
|
||
log_warn " Alloy 未安装,跳过启动"
|
||
else
|
||
# 验证 Alloy 配置文件
|
||
log " 验证 Alloy 配置文件..."
|
||
if alloy validate /etc/alloy/config.alloy 2>&1 | tee -a "${CONTAINER_LOGS_DIR:-/app/container-logs}/diag/alloy.log"; then
|
||
log_success " Alloy 配置文件验证通过"
|
||
else
|
||
log_warn " Alloy 配置文件验证失败,但仍将尝试启动"
|
||
fi
|
||
|
||
# 后台启动 Grafana Alloy
|
||
# 注意:eBPF 需要 root 权限,容器已通过 privileged 运行
|
||
log " 启动 Alloy 进程..."
|
||
nohup alloy run /etc/alloy/config.alloy \
|
||
> "${CONTAINER_LOGS_DIR:-/app/container-logs}/diag/alloy.log" 2>&1 &
|
||
|
||
local alloy_pid=$!
|
||
log_success " Grafana Alloy 已启动 (PID: $alloy_pid)"
|
||
log " 📊 性能数据发送到: $PYROSCOPE_URL"
|
||
log " 💡 Web UI: http://localhost:4040"
|
||
log " 🔍 监控进程: agent_runner 及其子进程"
|
||
log " 📝 日志文件: ${CONTAINER_LOGS_DIR:-/app/container-logs}/diag/alloy.log"
|
||
|
||
# 等待 3 秒后检查 Alloy 进程状态
|
||
sleep 3
|
||
if ps -p "$alloy_pid" > /dev/null 2>&1; then
|
||
log_success " Alloy 进程运行中"
|
||
# 显示最近的日志(用于调试)
|
||
log " 最近的 Alloy 日志:"
|
||
tail -n 5 "${CONTAINER_LOGS_DIR:-/app/container-logs}/diag/alloy.log" 2>/dev/null | while IFS= read -r line; do
|
||
log " $line"
|
||
done
|
||
else
|
||
log_warn " Alloy 进程已退出,请检查日志"
|
||
log " 错误日志:"
|
||
tail -n 10 "${CONTAINER_LOGS_DIR:-/app/container-logs}/diag/alloy.log" 2>/dev/null | while IFS= read -r line; do
|
||
log " $line"
|
||
done
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
log "📊 Grafana Alloy 已禁用 (设置 ENABLE_ALLOY=true 启用)"
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 🔍 Off-CPU 阻塞监控 (offcputime-bpfcc)
|
||
# ============================================================================
|
||
if [ "${ENABLE_OFFCPUTIME:-false}" = "true" ]; then
|
||
log "🔧 启动 Off-CPU 阻塞监控..."
|
||
|
||
# 检查 offcputime-bpfcc 是否可用
|
||
if ! command -v offcputime-bpfcc &> /dev/null; then
|
||
log_warn " offcputime-bpfcc 未安装,跳过 Off-CPU 监控"
|
||
else
|
||
# 创建 offcpu-monitor.sh 脚本(如果不存在)
|
||
if [ ! -f "/usr/local/bin/ebpf-tools/offcpu-monitor.sh" ]; then
|
||
log_warn " offcpu-monitor.sh 未找到,跳过 Off-CPU 监控"
|
||
else
|
||
# 启动 Off-CPU 监控
|
||
/usr/local/bin/ebpf-tools/offcpu-monitor.sh start &
|
||
local offcpu_monitor_pid=$!
|
||
log_success " Off-CPU 监控已启动 (PID: $offcpu_monitor_pid)"
|
||
log " 📊 阻塞火焰图将每 ${OFFCPU_INTERVAL:-300} 秒生成一次"
|
||
log " 💡 阻塞火焰图保存到: ${DIAG_OUTPUT_DIR:-/app/container-logs/diag}/offcpu-*.svg"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ============================================================================
|
||
# 🔍 系统调用监控 (syscalls)
|
||
# ============================================================================
|
||
if [ "${ENABLE_SYSCALL_MONITOR:-false}" = "true" ]; then
|
||
log "🔧 启动系统调用监控..."
|
||
|
||
# 检查 syscount-bpfcc 是否可用
|
||
if ! command -v syscount-bpfcc &> /dev/null; then
|
||
log_warn " syscount-bpfcc 未安装,跳过系统调用监控"
|
||
else
|
||
# 启动系统调用监控
|
||
/usr/local/bin/ebpf-tools/syscall-monitor.sh start &
|
||
local syscall_monitor_pid=$!
|
||
log_success " 系统调用监控已启动 (PID: $syscall_monitor_pid)"
|
||
log " 📊 系统调用统计将每 ${GENERATE_INTERVAL:-60} 秒生成一次"
|
||
log " 📝 日志文件: ${DIAG_OUTPUT_DIR:-/app/container-logs/diag}/syscall-monitor.log"
|
||
log " 💡 统计结果保存到: ${DIAG_OUTPUT_DIR:-/app/container-logs/diag}/syscall-count-*.txt"
|
||
fi
|
||
fi
|
||
|
||
# ========== 关键修复:确保 agent_runner 及其子进程继承输入法环境 ==========
|
||
# 从 /tmp/dbus-session-env 加载 D-Bus 地址
|
||
# ========== 创建全局输入法环境配置文件 ==========
|
||
# 所有进程(包括 agent_runner、chrome-devtools-mcp、Chromium)都会继承这些环境变量
|
||
cat > /etc/profile.d/ime-env.sh <<'EOF'
|
||
# Fcitx5 中文输入法环境变量
|
||
export DISPLAY=:0
|
||
export GTK_IM_MODULE=fcitx
|
||
export QT_IM_MODULE=fcitx
|
||
export XMODIFIERS=@im=fcitx
|
||
export INPUT_METHOD=fcitx
|
||
export LANG=C.UTF-8
|
||
export LC_ALL=C.UTF-8
|
||
export LC_CTYPE=C.UTF-8
|
||
EOF
|
||
|
||
# 加载 D-Bus 会话地址并追加到环境配置文件
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
export DBUS_SESSION_BUS_ADDRESS
|
||
log_success "agent_runner will use D-Bus: $DBUS_SESSION_BUS_ADDRESS"
|
||
|
||
# 将 D-Bus 地址追加到全局环境配置(注意:用 >> 追加,不是覆盖)
|
||
echo "export DBUS_SESSION_BUS_ADDRESS='${DBUS_SESSION_BUS_ADDRESS}'" >> /etc/profile.d/ime-env.sh
|
||
fi
|
||
|
||
# 立即加载环境配置
|
||
source /etc/profile.d/ime-env.sh
|
||
|
||
# 确保输入法环境变量被导出到当前 shell
|
||
export DISPLAY=:0
|
||
export GTK_IM_MODULE=fcitx
|
||
export QT_IM_MODULE=fcitx
|
||
export XMODIFIERS=@im=fcitx
|
||
export INPUT_METHOD=fcitx
|
||
export LANG=C.UTF-8
|
||
export LC_ALL=C.UTF-8
|
||
export LC_CTYPE=C.UTF-8
|
||
|
||
log_success "Input method environment variables exported globally (/etc/profile.d/ime-env.sh)"
|
||
|
||
# ========== 新增:将环境变量写入 /etc/environment(systemd 使用)==========
|
||
# 确保通过 systemd 启动的服务也能继承
|
||
cat >> /etc/environment <<EOF
|
||
DISPLAY=:0
|
||
GTK_IM_MODULE=fcitx
|
||
QT_IM_MODULE=fcitx
|
||
XMODIFIERS=@im=fcitx
|
||
INPUT_METHOD=fcitx
|
||
LANG=C.UTF-8
|
||
LC_ALL=C.UTF-8
|
||
LC_CTYPE=C.UTF-8
|
||
EOF
|
||
|
||
log_success "Input method environment variables written to /etc/environment"
|
||
|
||
# ============================================================================
|
||
# 🎯 启动 agent_runner
|
||
# ============================================================================
|
||
|
||
# 切换到用户主目录
|
||
cd /home/user
|
||
|
||
# 关键:显式设置 HOME 环境变量为 /home/user
|
||
# 虽然以 root 运行,但缓存和配置文件仍使用 /home/user 目录(挂载目录)
|
||
export HOME=/home/user
|
||
|
||
# 确保所有输入法环境变量已导出
|
||
source /etc/profile.d/ime-env.sh 2>/dev/null || true
|
||
|
||
# 等待 D-Bus 会话文件创建(智能等待,最长 5 秒)
|
||
wait_for_file /tmp/dbus-session-env 5 || log_warn "D-Bus session file not ready"
|
||
|
||
|
||
# ========== 等待 MCP Proxy 服务就绪 ==========
|
||
# MCP Proxy 已在后台并行启动,这里只需等待端口就绪
|
||
# 由于是并行启动,通常很快就会就绪
|
||
log "Waiting for MCP Proxy service to be ready..."
|
||
MCP_PROXY_PORT=18099
|
||
MCP_PROXY_TIMEOUT=30 # 并行启动后,超时时间从 60s 降至 30s
|
||
|
||
# 使用 wait_for_port 智能等待端口就绪
|
||
if wait_for_port 127.0.0.1 $MCP_PROXY_PORT $MCP_PROXY_TIMEOUT; then
|
||
# 端口就绪后,使用 curl 发送 JSON-RPC 请求验证 MCP 服务是否真正可用
|
||
# 注意:mcp-proxy convert 是持续运行的进程会导致 5 秒超时,改用 curl 直接测试 HTTP 端点
|
||
MCP_TEST_RESULT=$(curl -s --max-time 3 -X POST "http://127.0.0.1:$MCP_PROXY_PORT" \
|
||
-H "Content-Type: application/json" \
|
||
-H "Accept: application/json, text/event-stream" \
|
||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' 2>/dev/null)
|
||
|
||
if echo "$MCP_TEST_RESULT" | grep -q '"tools"'; then
|
||
log_success "MCP Proxy is fully ready on port $MCP_PROXY_PORT"
|
||
else
|
||
log_warn "MCP Proxy port is open but service not fully initialized, continuing anyway"
|
||
log_warn "Response: $MCP_TEST_RESULT"
|
||
fi
|
||
else
|
||
log_warn "MCP Proxy not ready after ${MCP_PROXY_TIMEOUT}s, starting agent_runner anyway"
|
||
log_warn "Agent may need to retry MCP connections on first use"
|
||
fi
|
||
|
||
# 加载 D-Bus 会话环境
|
||
if [ -f /tmp/dbus-session-env ]; then
|
||
source /tmp/dbus-session-env
|
||
log_success "Loaded D-Bus session: $DBUS_SESSION_BUS_ADDRESS"
|
||
fi
|
||
|
||
# 构建环境变量导出命令
|
||
ENV_EXPORTS="export HOME=/home/user; \
|
||
export DISPLAY=:0; \
|
||
export DBUS_SESSION_BUS_ADDRESS='${DBUS_SESSION_BUS_ADDRESS:-}'; \
|
||
export GTK_IM_MODULE=fcitx; \
|
||
export QT_IM_MODULE=fcitx; \
|
||
export XMODIFIERS=@im=fcitx; \
|
||
export INPUT_METHOD=fcitx; \
|
||
export LANG=C.UTF-8; \
|
||
export LC_ALL=C.UTF-8; \
|
||
export BROWSER=/usr/bin/chromium-browser-launcher; \
|
||
export PATH=/usr/local/bin:/opt/cargo/bin:\$PATH"
|
||
|
||
# 如果命令行传递了参数,则执行该参数(以 root 身份,但 HOME=/home/user)
|
||
# 否则执行默认的 agent_runner
|
||
if [ $# -gt 0 ]; then
|
||
log "Running custom command as root (HOME=/home/user): $*"
|
||
exec /bin/bash -c "$ENV_EXPORTS; exec $*"
|
||
else
|
||
# 默认启动 agent_runner (以 root 身份,但 HOME=/home/user)
|
||
log "Launching agent_runner as root (HOME=/home/user) on port ${PORT:-8086}..."
|
||
exec /bin/bash -c "$ENV_EXPORTS; exec /usr/local/bin/agent_runner -p ${PORT:-8086}"
|
||
fi
|