Установка на сервере владельца прервалась на проверке: «No module named 'fastapi'». Причин две, и обе в установщике. Первая: запуск через sudo. Установка пользовательская — всё ложится в $HOME/.hermes и $HOME/.local/bin, root не нужен. Под sudo домашним каталогом становится /root, венв Hermes там не находится, и установщик уходит на системный python. Копия при этом ложится в /root/.hermes, где владелец её не видит. Теперь запуск через sudo распознаётся и отклоняется с объяснением; осознанный обход остаётся через HERMES_ALLOW_ROOT=1. Вторая: системный python в Ubuntu 24.04 помечен EXTERNALLY-MANAGED (PEP 668) и отклоняет pip install — и обычный, и с --user. Установщик обе неудачи проглатывал (|| true) и продолжал работу до отказа на проверке. Теперь при защищённом системном python создаётся собственное окружение $HERMES_HOME/venv, а неудача установки зависимостей прекращает установку с внятным кодом возврата. --break-system-packages не применяется: имя флага не преувеличивает. Пусковик научен той же ветке — иначе запуск уходил бы на системный python, где зависимостей нет и быть не может. Проверено исполнением на сервере под непривилегированным пользователем: окружение создано, зависимости установлены, HERMES_HUB_LINUX_VERIFY_OK, запуск через sudo отклонён с кодом 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
7.6 KiB
Bash
182 lines
7.6 KiB
Bash
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# Hermes Hub — Linux Web Application Launcher
|
||
# Starts the background web server if not already active, waits for /api/health,
|
||
# and opens the UI in application window mode (--app=URL) or handles headless mode.
|
||
# ==============================================================================
|
||
|
||
set -e
|
||
|
||
DEFAULT_HERMES_HOME="$HOME/.hermes"
|
||
HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}"
|
||
|
||
# 1. Discover Python Runtime
|
||
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 закрыт
|
||
# правилом PEP 668. Без этой ветки запуск уходил бы на /usr/bin/python3,
|
||
# где зависимостей нет и быть не может.
|
||
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)"
|
||
else
|
||
echo "❌ Error: Python 3 not found on system." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 2. Configure Environment and Read Settings (Host/Port)
|
||
export PYTHONPATH="$HERMES_HOME/plugins/antigravity-provider/src:$HERMES_HOME/hermes-agent:$PYTHONPATH"
|
||
|
||
HOST="127.0.0.1"
|
||
PORT=5800
|
||
|
||
SETTINGS_FILE="$HERMES_HOME/hub_settings.json"
|
||
if [ -f "$SETTINGS_FILE" ]; then
|
||
PARSED_PORT="$("$PYTHON_BIN" -c "import json; print(json.load(open('$SETTINGS_FILE')).get('web_api_port', 5800))" 2>/dev/null || echo 5800)"
|
||
if [ -n "$PARSED_PORT" ] && [ "$PARSED_PORT" -gt 0 ] 2>/dev/null; then
|
||
PORT="$PARSED_PORT"
|
||
fi
|
||
PARSED_HOST="$("$PYTHON_BIN" -c "import json; h = json.load(open('$SETTINGS_FILE')).get('web_api_host', '127.0.0.1'); print('127.0.0.1' if h in ('0.0.0.0', '') else h)" 2>/dev/null || echo '127.0.0.1')"
|
||
if [ -n "$PARSED_HOST" ]; then
|
||
HOST="$PARSED_HOST"
|
||
fi
|
||
fi
|
||
|
||
TARGET_URL="http://$HOST:$PORT/"
|
||
HEALTH_URL="http://$HOST:$PORT/api/health"
|
||
|
||
# Function to check server health
|
||
check_health() {
|
||
# sys импортируется здесь, а не внутри except: раньше на УСПЕШНОМ ответе
|
||
# возникал NameError, его ловил тот же except, и проверка всегда сообщала
|
||
# об отказе — сервер работал, а лаунчер писал «failed to respond».
|
||
"$PYTHON_BIN" -c "
|
||
import json
|
||
import sys
|
||
import urllib.request
|
||
|
||
try:
|
||
with urllib.request.urlopen('$HEALTH_URL', timeout=0.8) as resp:
|
||
data = json.loads(resp.read().decode('utf-8'))
|
||
sys.exit(0 if data.get('ok') is True else 1)
|
||
except Exception:
|
||
sys.exit(1)
|
||
" >/dev/null 2>&1
|
||
}
|
||
|
||
# 3. Start Web Server if not already active
|
||
SERVER_STARTED_BY_US=false
|
||
if ! check_health; then
|
||
echo "Starting Hermes Hub Web Server in background..."
|
||
LOG_FILE="$HERMES_HOME/hermes_web_server.log"
|
||
nohup "$PYTHON_BIN" -m antigravity_provider.router.web > "$LOG_FILE" 2>&1 &
|
||
SERVER_PID=$!
|
||
SERVER_STARTED_BY_US=true
|
||
|
||
# Wait for server readiness (polling /api/health)
|
||
READY=false
|
||
for i in $(seq 1 75); do
|
||
if check_health; then
|
||
READY=true
|
||
break
|
||
fi
|
||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||
echo "❌ Error: Web server process terminated unexpectedly. Check $LOG_FILE" >&2
|
||
exit 1
|
||
fi
|
||
sleep 0.2
|
||
done
|
||
|
||
if [ "$READY" != "true" ]; then
|
||
echo "❌ Error: Web server failed to respond at $HEALTH_URL within 15 seconds." >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# 4. Headless Server Check (SSH / No Graphical Display)
|
||
#
|
||
# Текст здесь раньше был зашит: он всегда советовал пробрасывать порт и
|
||
# показывал 127.0.0.1, даже когда хаб уже привязан к сети и доступен напрямую.
|
||
# Инструкция, не совпадающая с настройками, хуже отсутствующей — владелец
|
||
# поднимал туннель к серверу, который и так был виден.
|
||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
||
SETTINGS_FILE="$HERMES_HOME/hub_settings.json"
|
||
BIND_HOST="127.0.0.1"
|
||
if [ -f "$SETTINGS_FILE" ]; then
|
||
BIND_HOST="$("$PYTHON_BIN" - "$SETTINGS_FILE" <<'PYEOF' 2>/dev/null || echo "127.0.0.1"
|
||
import json, sys
|
||
try:
|
||
print(json.load(open(sys.argv[1], encoding="utf-8")).get("web_api_host", "127.0.0.1"))
|
||
except Exception:
|
||
print("127.0.0.1")
|
||
PYEOF
|
||
)"
|
||
fi
|
||
|
||
echo "======================================================================"
|
||
echo " Hermes Hub Web Server is running (no graphical display)"
|
||
echo ""
|
||
if [ "$BIND_HOST" = "127.0.0.1" ] || [ -z "$BIND_HOST" ]; then
|
||
echo " Listening on 127.0.0.1:$PORT — reachable from this machine only."
|
||
echo ""
|
||
echo " To open it from another computer, forward the port:"
|
||
echo " ssh -L $PORT:127.0.0.1:$PORT <user>@<this-host>"
|
||
echo " then open http://127.0.0.1:$PORT/ there."
|
||
echo ""
|
||
echo " To expose it on the local network instead:"
|
||
echo " python3 $HERMES_HOME/scripts/enable_lan_access.py"
|
||
else
|
||
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||
[ -n "$LAN_IP" ] || LAN_IP="<address of this host>"
|
||
echo " Listening on $BIND_HOST:$PORT — reachable over the network."
|
||
echo ""
|
||
echo " Open from another computer:"
|
||
echo " http://$LAN_IP:$PORT/"
|
||
echo ""
|
||
echo " A token is required: paste it in Settings on first open."
|
||
echo " No port forwarding needed."
|
||
fi
|
||
echo "======================================================================"
|
||
exit 0
|
||
fi
|
||
|
||
# 5. Graphical Environment: Search for Chromium-based browser in priority order
|
||
CHROMIUM_BIN=""
|
||
|
||
for b in google-chrome google-chrome-stable chromium chromium-browser microsoft-edge microsoft-edge-stable brave-browser; do
|
||
if command -v "$b" >/dev/null 2>&1; then
|
||
CHROMIUM_BIN="$(command -v "$b")"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ -n "$CHROMIUM_BIN" ]; then
|
||
# Launch in Application Window mode without address bar, tabs, or menus
|
||
# Отдельный профиль: иначе запущенный браузер передаёт окно уже
|
||
# работающему экземпляру и сразу завершается — ожидание его закрытия
|
||
# срабатывает мгновенно. На Windows это приводило к тому, что сервер
|
||
# убивали, пока страница ещё грузилась.
|
||
BROWSER_PROFILE="$HERMES_HOME/web_browser_profile"
|
||
mkdir -p "$BROWSER_PROFILE"
|
||
"$CHROMIUM_BIN" --app="$TARGET_URL" --window-size=1400,900 --user-data-dir="$BROWSER_PROFILE" --no-first-run --no-default-browser-check "$@"
|
||
else
|
||
# Fallback to standard default browser
|
||
echo "Запуск в обычном браузере: режим приложения (без адресной строки) требует Google Chrome, Chromium или Microsoft Edge."
|
||
if command -v xdg-open >/dev/null 2>&1; then
|
||
xdg-open "$TARGET_URL"
|
||
elif command -v sensible-browser >/dev/null 2>&1; then
|
||
sensible-browser "$TARGET_URL"
|
||
elif command -v firefox >/dev/null 2>&1; then
|
||
firefox "$TARGET_URL"
|
||
else
|
||
echo "Please open $TARGET_URL in your web browser."
|
||
fi
|
||
fi
|
||
|
||
exit 0
|