hermes-hub/installer/install-linux.sh
ochenstarik-ui fc4707d9d9 fix(linux): значок .desktop-записи не рендерился; хаб нельзя было остановить
Найдено и проверено живым прогоном на этой машине (реальный 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>
2026-09-03 22:02:43 +07:00

332 lines
15 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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