mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-23 01:46:37 +08:00
feat(fnos): run entirely without root and harden process lifecycle
- Switch privilege model to run-as: package (zero root, per fnOS guide) - Borrow App Store python312 instead of bundling CPython; keep bundled uv - Move venv, HOME and caches onto the persistent data share - Start service via setsid; stop/upgrade kill the whole process group - Sweep stray runtime/box orphans in install/upgrade init hooks - Track LangBot 4.10.11 (manifest baseline + upstream merge)
This commit is contained in:
@@ -19,7 +19,30 @@ cd "${APP_DIR}" || {
|
||||
}
|
||||
|
||||
# --- Ensure data directory exists ---
|
||||
mkdir -p "${DATA_DIR}/plugins" "${DATA_DIR}/box" "${DATA_DIR}/logs" 2>/dev/null || true
|
||||
mkdir -p "${DATA_DIR}/plugins" "${DATA_DIR}/box" "${DATA_DIR}/logs" || {
|
||||
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Writable HOME and tool caches ---
|
||||
# Under run-as: package the generated user's system HOME and the app install
|
||||
# dir (TRIM_APPDEST) can be read-only; uv aborts if it cannot initialise its
|
||||
# cache. Point HOME / UV_CACHE_DIR at the persistent data share and keep the
|
||||
# venv there too (UV_PROJECT_ENVIRONMENT), instead of inside APP_DIR.
|
||||
VENV_DIR="${DATA_DIR}/.venv"
|
||||
export HOME="${DATA_DIR}/.home"
|
||||
export UV_CACHE_DIR="${DATA_DIR}/.cache/uv"
|
||||
# Never download a managed CPython (the download host is unreachable on many
|
||||
# NAS networks); use the distro Python only — fail loudly if it is missing.
|
||||
export UV_PYTHON_DOWNLOADS=never
|
||||
mkdir -p "${HOME}" "${UV_CACHE_DIR}" || {
|
||||
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Real uv/pip errors are captured here for diagnosis (the installer popup
|
||||
# only shows the short message we write to TRIM_TEMP_LOGFILE).
|
||||
DEBUG_LOG="${DATA_DIR}/logs/install-debug.log"
|
||||
|
||||
# --- Pre-seed config.yaml with the user-selected web port ---
|
||||
# Data root points at the persistent share (LANGBOT_DATA_ROOT is exported by
|
||||
@@ -35,7 +58,10 @@ TEMPLATE_FILE="${APP_DIR}/src/langbot/templates/config.yaml"
|
||||
_patch_config() {
|
||||
local cfg_dir="$1"
|
||||
local cfg_file="${cfg_dir}/config.yaml"
|
||||
mkdir -p "${cfg_dir}" 2>/dev/null || true
|
||||
mkdir -p "${cfg_dir}" || {
|
||||
echo "Cannot create config directory: ${cfg_dir}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
}
|
||||
if [ ! -f "${cfg_file}" ] && [ -f "${TEMPLATE_FILE}" ]; then
|
||||
cp "${TEMPLATE_FILE}" "${cfg_file}"
|
||||
fi
|
||||
@@ -68,15 +94,27 @@ if [ ! -d "/var/apps/nodejs_v${NODE_VERSION}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Python check ---
|
||||
PYTHON_BIN="python3"
|
||||
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
fi
|
||||
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
|
||||
echo "Python not found on this system" > "${TRIM_TEMP_LOGFILE}"
|
||||
# --- CPU architecture (must be resolved before locating bundled binaries) ---
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64|aarch64) ;;
|
||||
*)
|
||||
echo "Unsupported CPU architecture: ${ARCH}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Python: official python312 App Store app (same borrow pattern as Node.js) ---
|
||||
PYTHON_APP="python312"
|
||||
PYTHON_BIN="/var/apps/${PYTHON_APP}/target/bin/python3"
|
||||
if [ ! -d "/var/apps/${PYTHON_APP}" ]; then
|
||||
echo "未找到官方 Python 环境:请先在应用中心安装 ${PYTHON_APP},再重新安装本应用。" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
fi
|
||||
[ -x "${PYTHON_BIN}" ] || {
|
||||
echo "Python 解释器缺失或不可执行:${PYTHON_BIN}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
PY_VER=$("${PYTHON_BIN}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
||||
if [ -z "${PY_VER}" ]; then
|
||||
@@ -90,55 +128,32 @@ if [ "${PY_MAJOR}" -lt 3 ] || { [ "${PY_MAJOR}" -eq 3 ] && [ "${PY_MINOR}" -lt 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Resolve uv: bundled binary first, then online fallbacks ---
|
||||
UV_BIN=""
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64) BUNDLED_UV="${TRIM_APPDEST}/bin/uv-x86_64" ;;
|
||||
aarch64) BUNDLED_UV="${TRIM_APPDEST}/bin/uv-aarch64" ;;
|
||||
*) BUNDLED_UV="" ;;
|
||||
esac
|
||||
|
||||
if [ -n "${BUNDLED_UV}" ] && [ -x "${BUNDLED_UV}" ]; then
|
||||
mkdir -p "${TRIM_PKGVAR}/bin"
|
||||
cp "${BUNDLED_UV}" "${TRIM_PKGVAR}/bin/uv" && chmod +x "${TRIM_PKGVAR}/bin/uv"
|
||||
UV_BIN="${TRIM_PKGVAR}/bin/uv"
|
||||
fi
|
||||
|
||||
if [ -z "${UV_BIN}" ] && command -v uv >/dev/null 2>&1; then
|
||||
UV_BIN="uv"
|
||||
fi
|
||||
|
||||
if [ -z "${UV_BIN}" ]; then
|
||||
"${PYTHON_BIN}" -m pip install --user --no-cache-dir uv 2>/dev/null || \
|
||||
"${PYTHON_BIN}" -m pip install --no-cache-dir uv 2>/dev/null || \
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null || true
|
||||
export PATH="${HOME}/.local/bin:${PATH}"
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
UV_BIN="uv"
|
||||
elif [ -x "${HOME}/.local/bin/uv" ]; then
|
||||
UV_BIN="${HOME}/.local/bin/uv"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${UV_BIN}" ]; then
|
||||
echo "无法获取 uv:内置二进制缺失且在线安装失败。请检查网络后重新安装。" > "${TRIM_TEMP_LOGFILE}"
|
||||
# --- Resolve uv: run the bundled binary in place (single canonical path) ---
|
||||
UV_BIN="${TRIM_APPDEST}/bin/uv-${ARCH}"
|
||||
[ -x "${UV_BIN}" ] || {
|
||||
echo "Bundled uv binary missing or not executable: ${UV_BIN}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Create venv via uv ---
|
||||
if [ ! -d ".venv" ]; then
|
||||
"${UV_BIN}" venv .venv --python "${PYTHON_BIN}" || {
|
||||
echo "Failed to create Python virtual environment via uv" > "${TRIM_TEMP_LOGFILE}"
|
||||
# --- Create venv via uv (on the writable data share, not APP_DIR) ---
|
||||
if [ ! -d "${VENV_DIR}" ]; then
|
||||
{
|
||||
echo "== whoami: $(id 2>&1)"
|
||||
echo "== APP_DIR perms: $(ls -ld "${APP_DIR}" 2>&1)"
|
||||
echo "== DATA_DIR perms: $(ls -ld "${DATA_DIR}" 2>&1)"
|
||||
echo "== HOME=${HOME} UV_CACHE_DIR=${UV_CACHE_DIR}"
|
||||
"${UV_BIN}" venv "${VENV_DIR}" --python "${PYTHON_BIN}"
|
||||
} >> "${DEBUG_LOG}" 2>&1 || {
|
||||
echo "Failed to create Python virtual environment via uv. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# --- Sync dependencies ---
|
||||
"${UV_BIN}" sync --extra seekdb || {
|
||||
echo "Dependency sync failed. Check network connectivity." > "${TRIM_TEMP_LOGFILE}"
|
||||
# --- Sync dependencies (install into the relocated venv) ---
|
||||
if ! UV_PROJECT_ENVIRONMENT="${VENV_DIR}" "${UV_BIN}" sync --extra seekdb >> "${DEBUG_LOG}" 2>&1; then
|
||||
echo "Dependency sync failed. Check network connectivity. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# --- Verify frontend dist ---
|
||||
if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
||||
@@ -146,4 +161,9 @@ if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Prepare a writable HOME inside the data dir for tool caches (uv,
|
||||
# npm/npx MCP). Everything already runs as the package user (run-as:
|
||||
# package), so no chown is needed — files created here belong to it.
|
||||
mkdir -p "${DATA_DIR}/.home" 2>/dev/null || true
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user