Merge pull request #6 from ochenstarik-ui/installer/a61-port-live-fixes
fix(installer,release): находки agy с живой Windows-машины (A61)
This commit is contained in:
commit
3235239d5b
11 changed files with 409 additions and 67 deletions
|
|
@ -86,7 +86,17 @@ namespace HermesHubSetup
|
|||
}
|
||||
}
|
||||
|
||||
string defaultTarget = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\HermesHub");
|
||||
// GetFolderPath(LocalApplicationData) не всегда возвращает то, что
|
||||
// ждёт установщик — найдено живым прогоном (A61): в некоторых
|
||||
// окружениях (изолированный тестовый профиль, нестандартный
|
||||
// пользовательский куст реестра) значение расходится с
|
||||
// фактическим %LOCALAPPDATA%. Читаем переменную окружения первой.
|
||||
string localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
if (string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
}
|
||||
string defaultTarget = Path.Combine(localAppData, @"Programs\HermesHub");
|
||||
TargetInstallDir = defaultTarget;
|
||||
|
||||
// Check if already installed
|
||||
|
|
@ -595,6 +605,12 @@ namespace HermesHubSetup
|
|||
|
||||
private static void CreateStartMenuShortcut()
|
||||
{
|
||||
// Изолированные прогоны (HERMES_HUB_NO_REGISTRY=1) уже не пишут в
|
||||
// реестр (см. HERMES_HUB_NO_REGISTRY ниже), но ярлык в настоящем
|
||||
// меню Пуск владельца этим не перекрывался — найдено живым
|
||||
// прогоном тестов на A61: /silent-тест с этой переменной всё
|
||||
// равно оставлял значок в реальном Пуск.
|
||||
if (Environment.GetEnvironmentVariable("HERMES_HUB_NO_REGISTRY") == "1") return;
|
||||
try
|
||||
{
|
||||
string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
|
||||
|
|
@ -641,6 +657,7 @@ namespace HermesHubSetup
|
|||
|
||||
private static void RemoveStartMenuShortcut()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("HERMES_HUB_NO_REGISTRY") == "1") return;
|
||||
try
|
||||
{
|
||||
string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
|
||||
|
|
|
|||
|
|
@ -49,51 +49,8 @@ echo ""
|
|||
# крутить старый код в памяти, и владелец видел прежний интерфейс при новом
|
||||
# номере сборки. Три сборки подряд ставились в файлы, но не в работу.
|
||||
echo "[0/6] Остановка работающего Hermes Hub..."
|
||||
|
||||
stop_running_hub() {
|
||||
local pattern="antigravity_provider.router.web|hermes_hub_web_entry"
|
||||
local pids
|
||||
# Только процессы ЭТОГО пользователя и только те, что относятся к хабу.
|
||||
pids="$(pgrep -u "$(id -u)" -f "$pattern" 2>/dev/null | tr '
|
||||
' ' ')"
|
||||
|
||||
if [ -z "$pids" ]; then
|
||||
echo " Работающий хаб не найден — останавливать нечего."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Найдены процессы хаба: $pids"
|
||||
# shellcheck disable=SC2086
|
||||
kill $pids 2>/dev/null || true
|
||||
|
||||
local waited=0
|
||||
while [ "$waited" -lt 10 ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
pids="$(pgrep -u "$(id -u)" -f "$pattern" 2>/dev/null | tr '
|
||||
' ' ')"
|
||||
[ -z "$pids" ] && break
|
||||
done
|
||||
|
||||
if [ -n "$pids" ]; then
|
||||
echo " Не завершились за 10 секунд, снимаю принудительно: $pids"
|
||||
# shellcheck disable=SC2086
|
||||
kill -9 $pids 2>/dev/null || true
|
||||
sleep 1
|
||||
pids="$(pgrep -u "$(id -u)" -f "$pattern" 2>/dev/null | tr '
|
||||
' ' ')"
|
||||
fi
|
||||
|
||||
if [ -n "$pids" ]; then
|
||||
# Не прерываем установку: файлы обновятся, а владельцу скажем правду.
|
||||
echo " ⚠ Остались процессы: $pids. Снимите их вручную, иначе будет работать старый код."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Хаб остановлен."
|
||||
return 0
|
||||
}
|
||||
|
||||
# shellcheck source=./lib_stop_running_hub.sh
|
||||
. "$SCRIPT_DIR/lib_stop_running_hub.sh"
|
||||
stop_running_hub || true
|
||||
echo ""
|
||||
|
||||
|
|
@ -266,9 +223,35 @@ 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/HermesHub.ico"
|
||||
ICON_PATH="$HERMES_HOME/plugins/antigravity-provider/assets/branding/app/app_icon_256.png"
|
||||
if [ ! -f "$ICON_PATH" ]; then
|
||||
ICON_PATH="utilities-terminal"
|
||||
fi
|
||||
|
|
@ -289,6 +272,28 @@ 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
|
||||
|
|
|
|||
54
installer/lib_stop_running_hub.sh
Normal file
54
installer/lib_stop_running_hub.sh
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Hermes Hub — общая функция остановки работающего хаба (Linux/POSIX).
|
||||
#
|
||||
# До этого файла одна и та же функция была отдельно вписана в install-linux.sh
|
||||
# и в uninstall-linux.sh — две копии, которые разошлись бы при первой же
|
||||
# правке одной из них незамеченной для другой. Источник источается («source»)
|
||||
# обоими скриптами и лаунчером остановки, поэтому логика одна.
|
||||
#
|
||||
# Использование: `source "$(dirname "$0")/lib_stop_running_hub.sh"`, затем
|
||||
# вызвать `stop_running_hub`. Функция сама печатает ход дела и возвращает
|
||||
# 0 (остановлен или нечего было останавливать) либо 1 (что-то осталось —
|
||||
# вызывающий решает, прерывать ли из-за этого).
|
||||
# ==============================================================================
|
||||
|
||||
stop_running_hub() {
|
||||
local pattern="antigravity_provider.router.web|hermes_hub_web_entry"
|
||||
local pids
|
||||
# Только процессы ЭТОГО пользователя и только те, что относятся к хабу.
|
||||
pids="$(pgrep -u "$(id -u)" -f "$pattern" 2>/dev/null | tr '\n' ' ')"
|
||||
|
||||
if [ -z "$pids" ]; then
|
||||
echo " Работающий хаб не найден — останавливать нечего."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Найдены процессы хаба: $pids"
|
||||
# shellcheck disable=SC2086
|
||||
kill $pids 2>/dev/null || true
|
||||
|
||||
local waited=0
|
||||
while [ "$waited" -lt 10 ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
pids="$(pgrep -u "$(id -u)" -f "$pattern" 2>/dev/null | tr '\n' ' ')"
|
||||
[ -z "$pids" ] && break
|
||||
done
|
||||
|
||||
if [ -n "$pids" ]; then
|
||||
echo " Не завершились за 10 секунд, снимаю принудительно: $pids"
|
||||
# shellcheck disable=SC2086
|
||||
kill -9 $pids 2>/dev/null || true
|
||||
sleep 1
|
||||
pids="$(pgrep -u "$(id -u)" -f "$pattern" 2>/dev/null | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
if [ -n "$pids" ]; then
|
||||
echo " ⚠ Остались процессы: $pids. Снимите их вручную."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Хаб остановлен."
|
||||
return 0
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
DEFAULT_HERMES_HOME="$HOME/.hermes"
|
||||
HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}"
|
||||
|
||||
|
|
@ -23,18 +25,35 @@ echo "======================================================================"
|
|||
echo "Hermes Home : $HERMES_HOME"
|
||||
echo ""
|
||||
|
||||
# 0. Остановка работающего хаба.
|
||||
#
|
||||
# Тот же порядок, что в install-linux.sh, и по той же причине: файлы под
|
||||
# работающим процессом здесь не просто устаревают, а исчезают. С
|
||||
# --purge-user-data это ещё и rm -rf каталогов, на которые у живого процесса
|
||||
# открыты файловые дескрипторы — на Linux это не роняет процесс, но он
|
||||
# продолжает отвечать по старому порту после «успешного» удаления, и
|
||||
# следующая попытка что-то с ним сделать бьётся об уже удалённые файлы.
|
||||
echo "[0/4] Остановка работающего Hermes Hub..."
|
||||
# shellcheck source=./lib_stop_running_hub.sh
|
||||
. "$SCRIPT_DIR/lib_stop_running_hub.sh"
|
||||
stop_running_hub || true
|
||||
echo ""
|
||||
|
||||
# 1. Remove Plugin Integration
|
||||
echo "[1/3] Removing plugin integration..."
|
||||
echo "[1/4] Removing plugin integration..."
|
||||
if [ -d "$HERMES_HOME/plugins/antigravity-provider" ]; then
|
||||
rm -rf "$HERMES_HOME/plugins/antigravity-provider"
|
||||
echo " Removed $HERMES_HOME/plugins/antigravity-provider"
|
||||
fi
|
||||
|
||||
# 2. Remove Launchers and Shortcuts
|
||||
echo "[2/3] Removing application launchers and desktop entries..."
|
||||
echo "[2/4] Removing application launchers and desktop entries..."
|
||||
rm -f "$HOME/.local/bin/hermes-hub-web"
|
||||
rm -f "$HERMES_HOME/bin/hermes-hub-web"
|
||||
rm -f "$HOME/.local/bin/hermes-hub-stop"
|
||||
rm -f "$HOME/.local/bin/lib_stop_running_hub.sh"
|
||||
rm -f "$HOME/.local/share/applications/hermes-hub-web.desktop"
|
||||
rm -f "$HOME/.local/share/applications/hermes-hub-stop.desktop"
|
||||
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
||||
|
|
@ -42,17 +61,26 @@ fi
|
|||
|
||||
# 3. User Data Handling
|
||||
if [ "$PURGE_USER_DATA" = "true" ]; then
|
||||
echo "[3/3] Purging user data (--purge-user-data specified)..."
|
||||
echo "[3/4] Purging user data (--purge-user-data specified)..."
|
||||
rm -f "$HERMES_HOME/config/router_profiles.yaml"
|
||||
rm -rf "$HERMES_HOME/agy_profiles"
|
||||
rm -rf "$HERMES_HOME/codex_profiles"
|
||||
rm -rf "$HERMES_HOME/opencode_profiles"
|
||||
echo " User configuration and profiles purged."
|
||||
else
|
||||
echo "[3/3] Preserving user data and credentials."
|
||||
echo "[3/4] Preserving user data and credentials."
|
||||
echo " Your router profiles, auth keys, and settings in $HERMES_HOME remain intact."
|
||||
fi
|
||||
|
||||
# 4. Post-uninstall verification: пойманный хаб действительно молчит.
|
||||
echo "[4/4] Verifying no hub process remains..."
|
||||
REMAINING="$(pgrep -u "$(id -u)" -f "antigravity_provider.router.web|hermes_hub_web_entry" 2>/dev/null | tr '\n' ' ')"
|
||||
if [ -n "$REMAINING" ]; then
|
||||
echo " ⚠ Всё ещё работает: $REMAINING — удаление файлов это не остановило."
|
||||
else
|
||||
echo " Хаб не работает."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "======================================================================"
|
||||
echo " HERMES HUB UNINSTALLED SUCCESSFULLY FROM LINUX "
|
||||
|
|
|
|||
49
launcher/hermes-hub-stop.sh
Normal file
49
launcher/hermes-hub-stop.sh
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Hermes Hub — Stop (Linux)
|
||||
#
|
||||
# На Windows фоновый сервер запускается из HermesHubWeb.exe, который держит
|
||||
# значок в системном трее — оттуда «Exit» останавливает процесс. На Linux
|
||||
# сервер стартует через nohup и остаётся в фоне после закрытия окна браузера
|
||||
# (так и задумано: не переустанавливать при каждом перезапуске окна), но
|
||||
# остановить его после этого было решительно нечем — ни кнопки в интерфейсе
|
||||
# (её нет ни на одной платформе), ни трея, ни пункта меню. Только терминал и
|
||||
# pkill вручную, либо переустановка/удаление, которые останавливают хаб
|
||||
# только как побочный эффект.
|
||||
#
|
||||
# Этот скрипт — тот недостающий эквивалент «Exit из трея»: доступен из меню
|
||||
# приложений через собственный .desktop-пункт, использует ту же проверенную
|
||||
# функцию остановки, что installer/install-linux.sh и uninstall-linux.sh.
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Устанавливается рядом (в ~/.hermes/bin) install-linux.sh — оттуда и берём
|
||||
# общую функцию. Если скрипт запущен не из установленного места (например,
|
||||
# прямо из репозитория), ищем installer/ на уровень выше.
|
||||
LIB=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/lib_stop_running_hub.sh" \
|
||||
"$SCRIPT_DIR/../installer/lib_stop_running_hub.sh"
|
||||
do
|
||||
if [ -f "$candidate" ]; then
|
||||
LIB="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$LIB" ]; then
|
||||
echo "❌ Не найдена installer/lib_stop_running_hub.sh — переустановите Hermes Hub." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=../installer/lib_stop_running_hub.sh
|
||||
. "$LIB"
|
||||
|
||||
echo "Останавливаю Hermes Hub..."
|
||||
if stop_running_hub; then
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
|
|
@ -241,6 +241,13 @@ CHECKSUMS_ASSET_NAME = "checksums.txt"
|
|||
# ассет не должен превращать ворота в отказ в обслуживании самим себе.
|
||||
MAX_PACKAGE_BYTES = 512 * 1024 * 1024
|
||||
|
||||
# Нижняя граница размера установщика — защита от усечённой сборки. Найдено
|
||||
# живым прогоном на Windows (A61): собранный HermesHubSetup.exe считался
|
||||
# готовым к публикации даже будучи почти пустым — сборка прервалась, а файл
|
||||
# остался. 1 МБ — заведомо меньше любого настоящего установщика (несёт
|
||||
# исходники плагина вшитым ресурсом), но отличает пустышку от файла.
|
||||
MIN_PACKAGE_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
def is_publication_mode() -> bool:
|
||||
"""Требуется ли блокирующая проверка публикации."""
|
||||
|
|
@ -272,6 +279,21 @@ def _download_and_hash(url: str) -> tuple[str, int]:
|
|||
return digest.hexdigest(), size
|
||||
|
||||
|
||||
def _hash_local_file(path: Path) -> tuple[str, int]:
|
||||
"""Посчитать SHA-256 локального файла целиком. Размер — побочный продукт."""
|
||||
import hashlib
|
||||
digest = hashlib.sha256()
|
||||
size = 0
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(1024 * 256)
|
||||
if not chunk:
|
||||
break
|
||||
size += len(chunk)
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest(), size
|
||||
|
||||
|
||||
def _parse_checksums(text: str) -> dict[str, str]:
|
||||
"""Разобрать строки вида '<sha256> <имя файла>'."""
|
||||
table: dict[str, str] = {}
|
||||
|
|
@ -390,6 +412,12 @@ def check_publishable_assets(dist_dir: Path) -> tuple[bool, str]:
|
|||
на тех же двух дефектах, что и CI: каждый его прогон завершался ошибкой, а
|
||||
релизы публиковались мимо него. Как только тесты позеленели, случайная
|
||||
защита исчезла — поэтому набор проверяется явно.
|
||||
|
||||
Помимо присутствия файлов — размер и хеш КАЖДОГО найденного установщика
|
||||
против локального checksums.txt. Найдено живым прогоном на Windows
|
||||
(A61): сборка может прерваться на середине и оставить усечённый файл, а
|
||||
checksums.txt и сам установщик могут разойтись ещё до всякой публикации.
|
||||
Проверка одного присутствия этого не ловит.
|
||||
"""
|
||||
if not dist_dir.is_dir():
|
||||
return False, f"Каталог сборки не найден: {dist_dir}"
|
||||
|
|
@ -413,7 +441,26 @@ def check_publishable_assets(dist_dir: Path) -> tuple[bool, str]:
|
|||
f"installer/build_installer.ps1 и installer/build_installer_linux.sh"
|
||||
)
|
||||
|
||||
return True, f"Набор ассетов пригоден для публикации: {installers} + {CHECKSUMS_ASSET_NAME}"
|
||||
local_checksums = _parse_checksums((dist_dir / CHECKSUMS_ASSET_NAME).read_text(encoding="utf-8-sig", errors="replace"))
|
||||
verified = []
|
||||
for name in installers:
|
||||
actual_hash, size = _hash_local_file(dist_dir / name)
|
||||
if size < MIN_PACKAGE_BYTES:
|
||||
return False, (
|
||||
f"{name} подозрительно мал ({size} байт, ожидался хотя бы {MIN_PACKAGE_BYTES}) "
|
||||
f"— похоже на прерванную сборку"
|
||||
)
|
||||
expected_hash = local_checksums.get(name)
|
||||
if not expected_hash:
|
||||
return False, f"Для {name} нет строки в {CHECKSUMS_ASSET_NAME} — сверить хеш не с чем"
|
||||
if actual_hash != expected_hash:
|
||||
return False, (
|
||||
f"SHA-256 {name} не сошёлся с {CHECKSUMS_ASSET_NAME}: "
|
||||
f"файл {actual_hash}, записан {expected_hash}"
|
||||
)
|
||||
verified.append(f"{name} ({size} байт, SHA-256 сошёлся)")
|
||||
|
||||
return True, f"Набор ассетов пригоден для публикации: {', '.join(verified)}"
|
||||
|
||||
|
||||
def run_release_gate():
|
||||
|
|
|
|||
|
|
@ -148,8 +148,15 @@ def run_checks() -> int:
|
|||
# Проверяем изоляцию пути, а не побочное создание каталога: запрос пути
|
||||
# каталогов больше не плодит, иначе любая проверка засоряла бы диск
|
||||
# десятком пустых слотов.
|
||||
pdir = get_profile_env_dir("ag-w2")
|
||||
assert "ag-w2" in str(pdir)
|
||||
#
|
||||
# ID заведомо не боевой. Было "ag-w2" — на живой машине владельца это
|
||||
# существующий подключённый профиль, и "assert not pdir.exists()" падал
|
||||
# не из-за бага, а потому что каталог реального аккаунта и так был на
|
||||
# месте. Найдено прогоном на настоящей установке (A61): скрипт возвращал
|
||||
# код 12, хотя изоляция путей работала верно.
|
||||
probe_id = "ag-probe-isolation-test"
|
||||
pdir = get_profile_env_dir(probe_id)
|
||||
assert probe_id in str(pdir)
|
||||
assert "agy_profiles" in str(pdir)
|
||||
assert not pdir.exists(), "запрос пути не должен создавать каталог"
|
||||
print(f" [PASS] Profile directory isolated at {pdir} (не создан)")
|
||||
|
|
|
|||
|
|
@ -1031,6 +1031,19 @@ class UpdateManager:
|
|||
chosen_url = assets[linux_name]
|
||||
break
|
||||
|
||||
# Платформа не распознана штатной веткой (is_win разошёлся с тем, что
|
||||
# реально есть в релизе) — переберём любое известное имя установщика,
|
||||
# прежде чем сдаваться на .zip. Найдено живым прогоном (A61): без
|
||||
# этой подстраховки единственная опечатка в определении платформы
|
||||
# роняла обновление с «не найден подходящий файл», хотя нужный
|
||||
# установщик в релизе был.
|
||||
if not chosen_url:
|
||||
for known_name in ("hermes-hub-setup.sh", "install-linux.sh", "HermesHubSetup.exe"):
|
||||
if known_name in assets:
|
||||
chosen_asset_name = known_name
|
||||
chosen_url = assets[known_name]
|
||||
break
|
||||
|
||||
# Fallback to any .zip package in assets or manifest package_url
|
||||
if not chosen_url:
|
||||
for a_name, a_url in assets.items():
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
|
@ -39,12 +40,29 @@ def test_silent_installer_execution_with_hermes(tmp_path):
|
|||
if not SETUP_EXE.is_file():
|
||||
pytest.skip("HermesHubSetup.exe not built yet")
|
||||
|
||||
# Set up mock Hermes Agent structure in temp home
|
||||
# Set up mock Hermes Agent structure in temp home pointing to the active venv.
|
||||
#
|
||||
# Пустые touch-файлы python.exe/hermes.exe раньше делали живую проверку
|
||||
# зависимостей на установленной машине бессмысленной: файл существовал,
|
||||
# но не был исполняемым питоном, и по-настоящему установщик его не
|
||||
# запускал ни разу. Найдено прогоном на настоящей машине (A61): junction
|
||||
# (или symlink) на реальный venv делает эту проверку осмысленной —
|
||||
# запускается настоящий интерпретатор.
|
||||
agent_dir = tmp_path / "hermes" / "hermes-agent"
|
||||
venv_scripts = agent_dir / "venv" / "Scripts"
|
||||
venv_scripts.mkdir(parents=True, exist_ok=True)
|
||||
(venv_scripts / "python.exe").touch()
|
||||
(venv_scripts / "hermes.exe").touch()
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
real_venv = Path(sys.prefix)
|
||||
target_venv = agent_dir / "venv"
|
||||
try:
|
||||
import _winapi
|
||||
_winapi.CreateJunction(str(real_venv), str(target_venv))
|
||||
except Exception:
|
||||
try:
|
||||
os.symlink(str(real_venv), str(target_venv), target_is_directory=True)
|
||||
except Exception:
|
||||
venv_scripts = target_venv / "Scripts"
|
||||
venv_scripts.mkdir(parents=True, exist_ok=True)
|
||||
(venv_scripts / "python.exe").touch()
|
||||
(venv_scripts / "hermes.exe").touch()
|
||||
|
||||
env = dict(os.environ)
|
||||
env["HERMES_HOME"] = str(tmp_path / "hermes")
|
||||
|
|
@ -54,7 +72,7 @@ def test_silent_installer_execution_with_hermes(tmp_path):
|
|||
env["HERMES_HUB_NO_REGISTRY"] = "1"
|
||||
|
||||
res = subprocess.run([str(SETUP_EXE), "/silent"], env=env, capture_output=True, text=True)
|
||||
assert res.returncode == 0, f"Expected returncode 0, got {res.returncode}. Stderr: {res.stderr}"
|
||||
assert res.returncode == 0, f"Expected returncode 0, got {res.returncode}. Stderr: {res.stderr}. Stdout: {res.stdout}"
|
||||
|
||||
|
||||
@pytest.mark.installer
|
||||
|
|
|
|||
|
|
@ -87,6 +87,17 @@ def test_linux_installer_script_structure():
|
|||
assert "HERMES_HOME" in install_sh
|
||||
assert "antigravity-provider" in install_sh
|
||||
|
||||
# Значок .desktop-записи должен быть растровым (PNG/SVG), не .ico.
|
||||
#
|
||||
# Измерено на настоящем GTK-рабочем столе: GdkPixbuf.Pixbuf.new_from_file
|
||||
# на .ico падает с "Compressed icons are not supported", а .desktop-файл
|
||||
# с нерендерящейся иконкой меню приложений и файловый менеджер просто
|
||||
# показывают пустым — без ошибки, молча. HermesHub.ico в тех же ассетах
|
||||
# существует и раньше подставлялся сюда, поэтому мало проверить, что путь
|
||||
# не пуст — нужно, чтобы это не был именно .ico.
|
||||
icon_line = next(line for line in install_sh.splitlines() if line.startswith("ICON_PATH="))
|
||||
assert ".ico" not in icon_line, f"иконка .desktop-записи — .ico, GTK его не рендерит: {icon_line!r}"
|
||||
|
||||
|
||||
def test_linux_launcher_script_headless_and_app_mode():
|
||||
"""Verify hermes-hub-web.sh checks DISPLAY, prints SSH port forwarding on headless, and uses --app on desktop."""
|
||||
|
|
@ -111,3 +122,49 @@ def test_linux_uninstaller_preserves_user_data():
|
|||
assert "--purge-user-data" in uninstall_sh
|
||||
assert "Preserving user data and credentials" in uninstall_sh
|
||||
assert "hermes-hub-web.desktop" in uninstall_sh
|
||||
|
||||
|
||||
def test_linux_uninstaller_stops_running_hub():
|
||||
"""A61/A62: uninstall-linux.sh must stop the hub before deleting its files.
|
||||
|
||||
Раньше удаление не трогало работающий процесс: rm -rf уходил под живым
|
||||
сервером, а с --purge-user-data ещё и сносил каталоги, на которые у
|
||||
процесса были открыты файловые дескрипторы. Хаб продолжал отвечать по
|
||||
старому порту после «успешного» удаления. Измерено живым прогоном:
|
||||
сервер, запущенный в песочнице, оставался в списке процессов после
|
||||
uninstall-linux.sh до этой правки.
|
||||
"""
|
||||
uninstall_sh = (INSTALLER_DIR / "uninstall-linux.sh").read_text(encoding="utf-8")
|
||||
assert "lib_stop_running_hub.sh" in uninstall_sh
|
||||
assert "stop_running_hub" in uninstall_sh
|
||||
|
||||
|
||||
def test_linux_stop_launcher_exists_and_reuses_shared_logic():
|
||||
"""A61/A62: должен существовать способ остановить хаб не из терминала руками.
|
||||
|
||||
На Windows это «Exit» из системного трея HermesHubWeb.exe. На Linux до
|
||||
этого не было ничего — ни кнопки в интерфейсе (её нет ни на одной
|
||||
платформе), ни трея, ни пункта меню: сервер, оставленный в фоне после
|
||||
закрытия окна браузера, можно было остановить только pkill'ом из
|
||||
терминала. launcher/hermes-hub-stop.sh — недостающий эквивалент,
|
||||
устанавливается install-linux.sh как второй пункт меню приложений.
|
||||
"""
|
||||
stop_sh = (LAUNCHER_DIR / "hermes-hub-stop.sh").read_text(encoding="utf-8")
|
||||
assert "#!/usr/bin/env bash" in stop_sh
|
||||
assert "stop_running_hub" in stop_sh
|
||||
|
||||
lib_sh = (INSTALLER_DIR / "lib_stop_running_hub.sh").read_text(encoding="utf-8")
|
||||
assert "stop_running_hub()" in lib_sh
|
||||
# Общий источник, а не третья копия той же функции: install и uninstall
|
||||
# обязаны ссылаться на тот же файл, а не хранить свою версию.
|
||||
install_sh = (INSTALLER_DIR / "install-linux.sh").read_text(encoding="utf-8")
|
||||
uninstall_sh = (INSTALLER_DIR / "uninstall-linux.sh").read_text(encoding="utf-8")
|
||||
for script_name, script_text in (("install-linux.sh", install_sh), ("uninstall-linux.sh", uninstall_sh)):
|
||||
assert "lib_stop_running_hub.sh" in script_text, f"{script_name} не источает общую функцию"
|
||||
assert script_text.count("stop_running_hub() {") == 0, (
|
||||
f"{script_name} держит собственную копию функции вместо общего источника"
|
||||
)
|
||||
|
||||
assert "hermes-hub-stop" in install_sh, "install-linux.sh не разворачивает лаунчер остановки"
|
||||
assert "hermes-hub-stop.desktop" in install_sh, "у лаунчера остановки нет пункта меню"
|
||||
assert "hermes-hub-stop" in uninstall_sh, "uninstall-linux.sh не убирает лаунчер остановки"
|
||||
|
|
|
|||
|
|
@ -653,12 +653,59 @@ def test_publishable_assets_check_rejects_uninstallable_release(tmp_path):
|
|||
assert ok is False, "набор без checksums.txt признан пригодным"
|
||||
assert "checksums.txt" in msg
|
||||
|
||||
as_published_really = tmp_path / "dist_full"
|
||||
as_published_really.mkdir()
|
||||
for name in ("HermesHubSetup.exe", "hermes-hub-setup.sh", "checksums.txt"):
|
||||
(as_published_really / name).write_bytes(b"x")
|
||||
ok, msg = release_gate.check_publishable_assets(as_published_really)
|
||||
assert ok is True, msg
|
||||
|
||||
ok, msg = release_gate.check_publishable_assets(tmp_path / "нет-такого")
|
||||
assert ok is False, "отсутствующий каталог сборки должен быть отказом"
|
||||
|
||||
|
||||
def _write_installer_with_checksum(directory, name: str, content: bytes) -> None:
|
||||
"""Собрать <name> + checksums.txt с настоящим SHA-256 для теста."""
|
||||
import hashlib
|
||||
|
||||
(directory / name).write_bytes(content)
|
||||
digest = hashlib.sha256(content).hexdigest()
|
||||
(directory / "checksums.txt").write_text(f"{digest} {name}\n", encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_publishable_assets_check_verifies_size_and_hash(tmp_path):
|
||||
"""A61: присутствия файлов мало — размер и хеш проверяются по-настоящему.
|
||||
|
||||
Найдено живым прогоном на Windows: сборка может прерваться на середине и
|
||||
оставить усечённый файл, а checksums.txt и сам установщик — разойтись ещё
|
||||
до всякой публикации. Проверка одного присутствия этого не ловит.
|
||||
"""
|
||||
release_gate = _load_release_gate()
|
||||
|
||||
# 1. Настоящий размер, настоящий хеш — проходит.
|
||||
good = tmp_path / "dist_good"
|
||||
good.mkdir()
|
||||
_write_installer_with_checksum(good, "HermesHubSetup.exe", b"X" * release_gate.MIN_PACKAGE_BYTES)
|
||||
ok, msg = release_gate.check_publishable_assets(good)
|
||||
assert ok is True, msg
|
||||
assert "SHA-256 сошёлся" in msg
|
||||
|
||||
# 2. Файл меньше нижней границы — похоже на прерванную сборку.
|
||||
truncated = tmp_path / "dist_truncated"
|
||||
truncated.mkdir()
|
||||
_write_installer_with_checksum(truncated, "HermesHubSetup.exe", b"x" * 10)
|
||||
ok, msg = release_gate.check_publishable_assets(truncated)
|
||||
assert ok is False, "усечённый установщик признан пригодным к публикации"
|
||||
assert "мал" in msg
|
||||
|
||||
# 3. Хеш в checksums.txt не совпадает с настоящим файлом.
|
||||
mismatched = tmp_path / "dist_mismatch"
|
||||
mismatched.mkdir()
|
||||
content = b"Y" * release_gate.MIN_PACKAGE_BYTES
|
||||
(mismatched / "HermesHubSetup.exe").write_bytes(content)
|
||||
(mismatched / "checksums.txt").write_text("0" * 64 + " HermesHubSetup.exe\n", encoding="utf-8")
|
||||
ok, msg = release_gate.check_publishable_assets(mismatched)
|
||||
assert ok is False, "разошедшийся хеш признан пригодным к публикации"
|
||||
assert "не сошёлся" in msg
|
||||
|
||||
# 4. checksums.txt не называет установленный файл вовсе.
|
||||
unnamed = tmp_path / "dist_unnamed"
|
||||
unnamed.mkdir()
|
||||
(unnamed / "HermesHubSetup.exe").write_bytes(b"Z" * release_gate.MIN_PACKAGE_BYTES)
|
||||
(unnamed / "checksums.txt").write_text("0" * 64 + " other-file.bin\n", encoding="utf-8")
|
||||
ok, msg = release_gate.check_publishable_assets(unnamed)
|
||||
assert ok is False, "checksums.txt без нужной строки признан достаточным"
|
||||
|
|
|
|||
Loading…
Reference in a new issue