Найдено и проверено живым прогоном на этой машине (реальный GTK-рабочий стол, реальные профили agy) — не по чтению кода. 1. Значок .desktop-записи был .ico. Измерено: GdkPixbuf.Pixbuf.new_from_file на HermesHub.ico падает с "Compressed icons are not supported". .desktop-файл с нерендерящейся иконкой меню приложений и файловый менеджер просто показывают пустым — без ошибки, молча. PNG в тех же ассетах уже был и загружается (проверено: 256x256). Подставлен app_icon_256.png. 2. Хаб нельзя было остановить иначе как из терминала. На Windows сервер стартует из HermesHubWeb.exe, который держит значок в системном трее — оттуда «Exit» останавливает процесс. На Linux сервер остаётся в фоне после закрытия окна браузера (так и задумано — не переустанавливать каждый раз), но ни кнопки в интерфейсе (её нет ни на одной платформе), ни трея, ни пункта меню не было вовсе. launcher/hermes-hub-stop.sh — недостающий эквивалент «Exit из трея»: доступен из меню приложений через собственный .desktop-пункт, использует ту же функцию, что и install/uninstall. Проверено живым прогоном: сервер запущен, остановлен через новый лаунчер, curl после этого получает connection refused. 3. uninstall-linux.sh не останавливал работающий хаб перед удалением файлов — та же причина, что install уже чинил для установки: с --purge-user-data это ещё и rm -rf каталогов, на которые у живого процесса открыты файловые дескрипторы. Измерено: сервер, запущенный в песочнице, оставался в списке процессов после uninstall-linux.sh до этой правки. Добавлена та же остановка, тем же кодом. 4. stop_running_hub была вписана отдельно в install-linux.sh и uninstall-linux.sh — две копии, которые разошлись бы при правке одной незамеченной для другой. Вынесена в installer/lib_stop_running_hub.sh, источается обоими скриптами и новым лаунчером остановки. Всё проверено дважды: прямым запуском install-linux.sh/uninstall-linux.sh в изолированной песочнице (не ~/.hermes) и через собранный dist/hermes-hub-setup.sh — тот самый файл, который уходит в релиз. Тесты: 738 -> 740 passed, 2 skipped, 4 deselected. Новый тест на .ico падает на прежней версии install-linux.sh (проверено git stash) и проходит после фикса. ruff check . чисто. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
332 lines
15 KiB
Bash
332 lines
15 KiB
Bash
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# Hermes Hub — Linux Installation Script
|
||
# Mirrors plugin files, registers .desktop entry, creates application launcher,
|
||
# and supports both desktop and headless server environments.
|
||
# ==============================================================================
|
||
|
||
set -e
|
||
|
||
HUB_VERSION="0.1.3"
|
||
DEFAULT_HERMES_HOME="$HOME/.hermes"
|
||
HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}"
|
||
|
||
# Установка пользовательская: всё ложится в $HOME/.hermes и $HOME/.local/bin,
|
||
# службы не ставятся, root не нужен. Под sudo домашним каталогом становится
|
||
# /root, венв Hermes там не находится, установщик сваливается на системный
|
||
# python — а он в Ubuntu 24.04 закрыт для pip (PEP 668). В итоге установка
|
||
# уходит в /root/.hermes, где владелец её не видит, и падает на проверке.
|
||
# Молча ставить не туда нельзя, поэтому отказываемся сразу и по делу.
|
||
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && [ -z "${HERMES_ALLOW_ROOT:-}" ]; then
|
||
echo "❌ Установщик запущен через sudo." >&2
|
||
echo "" >&2
|
||
echo " Hermes Hub ставится в домашний каталог пользователя, а под sudo" >&2
|
||
echo " это /root — туда, где ни аккаунты, ни Hermes Agent не лежат." >&2
|
||
echo "" >&2
|
||
# $0 здесь — распакованная копия во временном каталоге, называть её
|
||
# владельцу бессмысленно: этого файла через минуту не будет.
|
||
echo " Запустите тот же установщик от своего имени, без sudo." >&2
|
||
echo "" >&2
|
||
echo " Если установка в /root действительно нужна, задайте" >&2
|
||
echo " HERMES_ALLOW_ROOT=1." >&2
|
||
exit 3
|
||
fi
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
||
echo "======================================================================"
|
||
echo " HERMES HUB INSTALLER (Linux / POSIX) "
|
||
echo "======================================================================"
|
||
echo "Version : $HUB_VERSION"
|
||
echo "Hermes Home : $HERMES_HOME"
|
||
echo "Source Root : $REPO_ROOT"
|
||
echo ""
|
||
|
||
# 0. Остановка работающего хаба.
|
||
#
|
||
# Установщик копировал файлы, но работающий сервер не трогал: он продолжал
|
||
# крутить старый код в памяти, и владелец видел прежний интерфейс при новом
|
||
# номере сборки. Три сборки подряд ставились в файлы, но не в работу.
|
||
echo "[0/6] Остановка работающего Hermes Hub..."
|
||
# shellcheck source=./lib_stop_running_hub.sh
|
||
. "$SCRIPT_DIR/lib_stop_running_hub.sh"
|
||
stop_running_hub || true
|
||
echo ""
|
||
|
||
# 1. Check / Discover Python Runtime and Hermes Environment
|
||
echo "[1/6] Checking Python and Hermes environment..."
|
||
PYTHON_BIN=""
|
||
|
||
if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then
|
||
PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3"
|
||
elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then
|
||
PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python"
|
||
elif [ -x "$HERMES_HOME/venv/bin/python3" ]; then
|
||
PYTHON_BIN="$HERMES_HOME/venv/bin/python3"
|
||
elif command -v python3 >/dev/null 2>&1; then
|
||
PYTHON_BIN="$(command -v python3)"
|
||
elif command -v python >/dev/null 2>&1; then
|
||
PYTHON_BIN="$(command -v python)"
|
||
fi
|
||
|
||
if [ -z "$PYTHON_BIN" ]; then
|
||
echo "❌ Error: Python 3 not found on system. Please install Python 3.9+." >&2
|
||
exit 10
|
||
fi
|
||
|
||
PY_VER="$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
|
||
echo " Using Python: $PYTHON_BIN (v$PY_VER)"
|
||
|
||
# Ensure Hermes Home Directory Structure
|
||
mkdir -p "$HERMES_HOME"
|
||
mkdir -p "$HERMES_HOME/config"
|
||
mkdir -p "$HERMES_HOME/plugins/antigravity-provider/src"
|
||
mkdir -p "$HERMES_HOME/plugins/antigravity-provider/assets"
|
||
mkdir -p "$HOME/.local/bin"
|
||
mkdir -p "$HOME/.local/share/applications"
|
||
|
||
# 2. Check and Install Dependencies
|
||
echo "[2/6] Verifying Python dependencies..."
|
||
DEPS_OK=true
|
||
"$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml; print('DEPS_OK')" >/dev/null 2>&1 || DEPS_OK=false
|
||
|
||
HUB_DEPS="fastapi uvicorn pydantic psutil pyyaml"
|
||
|
||
if [ "$DEPS_OK" != "true" ]; then
|
||
# Системный python в Debian и Ubuntu помечен как externally-managed
|
||
# (PEP 668) и отклоняет pip install — и обычный, и с --user. Обходить это
|
||
# через --break-system-packages нельзя: имя флага не преувеличивает, так
|
||
# ломают питон всей машины. Правильный ответ — собственный venv.
|
||
NEED_VENV=false
|
||
if [ -f "/usr/lib/python$PY_VER/EXTERNALLY-MANAGED" ] || [ -f "/usr/lib/python3/EXTERNALLY-MANAGED" ]; then
|
||
case "$PYTHON_BIN" in
|
||
*/venv/bin/*) : ;;
|
||
*) NEED_VENV=true ;;
|
||
esac
|
||
fi
|
||
|
||
if [ "$NEED_VENV" = "true" ]; then
|
||
echo " Системный Python защищён от изменений (PEP 668)."
|
||
echo " Создаю отдельное окружение: $HERMES_HOME/venv"
|
||
if ! "$PYTHON_BIN" -m venv "$HERMES_HOME/venv" 2>/dev/null; then
|
||
echo "❌ Не удалось создать виртуальное окружение." >&2
|
||
echo " Установите пакет python3-venv:" >&2
|
||
echo " sudo apt install python3-venv" >&2
|
||
exit 11
|
||
fi
|
||
PYTHON_BIN="$HERMES_HOME/venv/bin/python3"
|
||
echo " Using Python: $PYTHON_BIN"
|
||
fi
|
||
|
||
echo " Installing required packages ($HUB_DEPS)..."
|
||
# shellcheck disable=SC2086
|
||
if ! "$PYTHON_BIN" -m pip install --no-warn-script-location -q $HUB_DEPS; then
|
||
echo "❌ Не удалось установить зависимости через $PYTHON_BIN." >&2
|
||
echo " Установка прервана: без них хаб не запустится." >&2
|
||
exit 12
|
||
fi
|
||
fi
|
||
|
||
# Проверяем результат, а не код возврата pip: установка «прошла», а модуля нет —
|
||
# именно так предыдущая сборка дошла до проверки и упала на ней.
|
||
if ! "$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml" >/dev/null 2>&1; then
|
||
echo "❌ Зависимости не импортируются даже после установки ($PYTHON_BIN)." >&2
|
||
exit 13
|
||
fi
|
||
|
||
# 3. Mirror Plugin Files to ~/.hermes/plugins/antigravity-provider (with cleanup of stale files)
|
||
echo "[3/6] Deploying plugin source files (mirrored)..."
|
||
PLUGIN_SRC="$REPO_ROOT/src/antigravity_provider"
|
||
PLUGIN_DST="$HERMES_HOME/plugins/antigravity-provider/src/antigravity_provider"
|
||
|
||
if [ -d "$PLUGIN_SRC" ]; then
|
||
mkdir -p "$PLUGIN_DST"
|
||
# Use rsync with --delete if available, or python mirroring
|
||
if command -v rsync >/dev/null 2>&1; then
|
||
rsync -a --delete --exclude='__pycache__' --exclude='*.pyc' "$PLUGIN_SRC/" "$PLUGIN_DST/"
|
||
else
|
||
"$PYTHON_BIN" -c "
|
||
import os, shutil
|
||
src = r'$PLUGIN_SRC'
|
||
dst = r'$PLUGIN_DST'
|
||
if os.path.exists(dst):
|
||
shutil.rmtree(dst)
|
||
shutil.copytree(src, dst, ignore=shutil.ignore_patterns('__pycache__', '*.pyc'))
|
||
"
|
||
fi
|
||
else
|
||
echo "❌ Error: Source directory $PLUGIN_SRC not found!" >&2
|
||
exit 12
|
||
fi
|
||
|
||
# Deploy Assets
|
||
if [ -d "$REPO_ROOT/assets" ]; then
|
||
mkdir -p "$HERMES_HOME/plugins/antigravity-provider/assets"
|
||
cp -r "$REPO_ROOT/assets/"* "$HERMES_HOME/plugins/antigravity-provider/assets/" 2>/dev/null || true
|
||
fi
|
||
|
||
# Deploy helper scripts. Они лежат в поставке, но раньше не разворачивались,
|
||
# поэтому вспомогательных инструментов (например открытия хаба в домашнюю сеть)
|
||
# на установленной машине просто не оказывалось.
|
||
if [ -d "$REPO_ROOT/scripts" ]; then
|
||
mkdir -p "$HERMES_HOME/scripts"
|
||
cp -r "$REPO_ROOT/scripts/"*.py "$HERMES_HOME/scripts/" 2>/dev/null || true
|
||
cp -r "$REPO_ROOT/scripts/"*.sh "$HERMES_HOME/scripts/" 2>/dev/null || true
|
||
fi
|
||
|
||
# 4. Write Deployment Manifest
|
||
echo "[4/6] Writing deployment manifest..."
|
||
MANIFEST_FILE="$HERMES_HOME/plugins/antigravity-provider/deployment_manifest.json"
|
||
# Коммит берётся из BUILD_COMMIT (кладёт сборщик самораспаковывающегося
|
||
# установщика, где git недоступен), иначе — из репозитория. Запасного
|
||
# литерала нет намеренно: неизвестный коммит должен читаться как неизвестный,
|
||
# а не как чужой чужой номер сборки.
|
||
if [ -f "$REPO_ROOT/BUILD_COMMIT" ]; then
|
||
GIT_COMMIT="$(tr -d '
|
||
' < "$REPO_ROOT/BUILD_COMMIT")"
|
||
else
|
||
GIT_COMMIT="$(cd "$REPO_ROOT" 2>/dev/null && git rev-parse --short HEAD 2>/dev/null || echo 'не определён')"
|
||
fi
|
||
TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u)"
|
||
|
||
cat <<EOF > "$MANIFEST_FILE"
|
||
{
|
||
"version": "$HUB_VERSION",
|
||
"deployed_at": "$TIMESTAMP",
|
||
"git_commit": "$GIT_COMMIT",
|
||
"platform": "linux"
|
||
}
|
||
EOF
|
||
|
||
# Deploy Template Config only if not existing
|
||
if [ ! -f "$HERMES_HOME/config/router_profiles.yaml" ] && [ -f "$REPO_ROOT/config/router_profiles.example.yaml" ]; then
|
||
echo " Installing default router_profiles.yaml from template..."
|
||
cp "$REPO_ROOT/config/router_profiles.example.yaml" "$HERMES_HOME/config/router_profiles.yaml"
|
||
else
|
||
echo " Preserving existing user router_profiles.yaml."
|
||
fi
|
||
|
||
# 5. Deploy Launcher Executable and .desktop Entry
|
||
echo "[5/6] Creating application launcher and .desktop entry..."
|
||
LAUNCHER_SRC="$REPO_ROOT/launcher/hermes-hub-web.sh"
|
||
if [ ! -f "$LAUNCHER_SRC" ]; then
|
||
LAUNCHER_SRC="$SCRIPT_DIR/hermes-hub-web.sh"
|
||
fi
|
||
|
||
LAUNCHER_BIN="$HOME/.local/bin/hermes-hub-web"
|
||
cp "$LAUNCHER_SRC" "$LAUNCHER_BIN"
|
||
chmod +x "$LAUNCHER_BIN"
|
||
|
||
# Also place in ~/.hermes/bin for convenience
|
||
mkdir -p "$HERMES_HOME/bin"
|
||
cp "$LAUNCHER_SRC" "$HERMES_HOME/bin/hermes-hub-web"
|
||
chmod +x "$HERMES_HOME/bin/hermes-hub-web"
|
||
|
||
# Лаунчер остановки — эквивалент «Exit» из системного трея Windows.
|
||
#
|
||
# На Windows сервер стартует из HermesHubWeb.exe, который держит значок в
|
||
# трее: закрыть его оттуда может сам владелец. На Linux сервер после закрытия
|
||
# окна остаётся в фоне без единого способа его остановить — ни кнопки в
|
||
# интерфейсе (её нет ни на одной платформе), ни трея, ни пункта меню. Кладём
|
||
# lib_stop_running_hub.sh рядом со скриптом остановки: он ищет её сначала
|
||
# рядом с собой.
|
||
STOP_LAUNCHER_SRC="$REPO_ROOT/launcher/hermes-hub-stop.sh"
|
||
if [ ! -f "$STOP_LAUNCHER_SRC" ]; then
|
||
STOP_LAUNCHER_SRC="$SCRIPT_DIR/../launcher/hermes-hub-stop.sh"
|
||
fi
|
||
STOP_LAUNCHER_BIN="$HOME/.local/bin/hermes-hub-stop"
|
||
if [ -f "$STOP_LAUNCHER_SRC" ]; then
|
||
cp "$STOP_LAUNCHER_SRC" "$STOP_LAUNCHER_BIN"
|
||
chmod +x "$STOP_LAUNCHER_BIN"
|
||
cp "$SCRIPT_DIR/lib_stop_running_hub.sh" "$HOME/.local/bin/lib_stop_running_hub.sh"
|
||
fi
|
||
|
||
# Create .desktop file
|
||
#
|
||
# Иконка — PNG, не .ico. Измерено на настоящем GTK-рабочем столе:
|
||
# GdkPixbuf.Pixbuf.new_from_file на HermesHub.ico падает с "Compressed icons
|
||
# are not supported", а .desktop-файл с несуществующей или неподдерживаемой
|
||
# иконкой Nautilus и меню приложений просто показывают пустое место — без
|
||
# ошибки, молча. Значок был бы вечно пустым на любом GTK-окружении (GNOME,
|
||
# большинство производных). PNG в тех же ассетах уже есть и загружается.
|
||
DESKTOP_FILE="$HOME/.local/share/applications/hermes-hub-web.desktop"
|
||
ICON_PATH="$HERMES_HOME/plugins/antigravity-provider/assets/branding/app/app_icon_256.png"
|
||
if [ ! -f "$ICON_PATH" ]; then
|
||
ICON_PATH="utilities-terminal"
|
||
fi
|
||
|
||
cat <<EOF > "$DESKTOP_FILE"
|
||
[Desktop Entry]
|
||
Version=1.0
|
||
Type=Application
|
||
Name=Hermes Hub Web
|
||
GenericName=Multi-Agent & Multi-Provider Control Hub
|
||
Comment=Multi-Agent & Multi-Provider Control Hub for Hermes Agent
|
||
Exec=$LAUNCHER_BIN
|
||
Icon=$ICON_PATH
|
||
Terminal=false
|
||
Categories=Development;Utility;
|
||
StartupNotify=true
|
||
StartupWMClass=hermes-hub-web
|
||
EOF
|
||
|
||
chmod +x "$DESKTOP_FILE"
|
||
|
||
# Второй пункт меню — «Остановить». Terminal=true: без окна владелец не
|
||
# увидит, остановился ли хаб на самом деле, и не заметит «⚠ Остались
|
||
# процессы» из lib_stop_running_hub.sh, если что-то пошло не так.
|
||
if [ -f "$STOP_LAUNCHER_BIN" ]; then
|
||
STOP_DESKTOP_FILE="$HOME/.local/share/applications/hermes-hub-stop.desktop"
|
||
cat <<EOF > "$STOP_DESKTOP_FILE"
|
||
[Desktop Entry]
|
||
Version=1.0
|
||
Type=Application
|
||
Name=Stop Hermes Hub
|
||
GenericName=Stop the Hermes Hub background server
|
||
Comment=Останавливает фоновый сервер Hermes Hub
|
||
Exec=$STOP_LAUNCHER_BIN
|
||
Icon=$ICON_PATH
|
||
Terminal=true
|
||
Categories=Development;Utility;
|
||
StartupNotify=false
|
||
EOF
|
||
chmod +x "$STOP_DESKTOP_FILE"
|
||
fi
|
||
|
||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
||
fi
|
||
|
||
# 6. Post-install Smoke Test Verification
|
||
echo "[6/6] Running post-install verification smoke test..."
|
||
"$PYTHON_BIN" -c "
|
||
import sys
|
||
sys.path.insert(0, '$HERMES_HOME/plugins/antigravity-provider/src')
|
||
import antigravity_provider.router.web.server
|
||
print('HERMES_HUB_LINUX_VERIFY_OK')
|
||
" || {
|
||
echo "❌ Error: Post-install verification failed!" >&2
|
||
exit 14
|
||
}
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo " HERMES HUB SUCCESSFULLY INSTALLED ON LINUX! "
|
||
echo "======================================================================"
|
||
echo "Application Launcher : $LAUNCHER_BIN"
|
||
echo "Desktop Shortcut : $DESKTOP_FILE"
|
||
echo ""
|
||
echo "To launch the Web Application window:"
|
||
echo " $LAUNCHER_BIN"
|
||
echo ""
|
||
echo "Or open 'Hermes Hub Web' from your Applications menu."
|
||
echo ""
|
||
echo "ВАЖНО: работавший хаб был остановлен перед установкой, иначе он"
|
||
echo "продолжал бы выполнять прежний код из памяти. Запустите его заново"
|
||
echo "командой выше — только тогда новая сборка начнёт работать."
|
||
echo ""
|
||
echo "Проверить, что поднялся новый код:"
|
||
echo " curl -s -D - -o /dev/null http://127.0.0.1:5800/workspace.js | grep -i cache-control"
|
||
echo "======================================================================"
|
||
exit 0
|