fix(antigravity): терминал не открывался из-за подменённого HOME; видно работающую сборку
Мастер честно сообщил: «Терминал /usr/bin/xfce4-terminal завершился сразу с кодом 1, окно не открылось» — новая проверка запуска сработала. Причина в моей же правке. Терминал запускался с HOME, подменённым на каталог профиля, а клиенты X11 берут ключ авторизации из ~/.Xauthority: в agy_profiles/ag-5 такого файла нет и быть не может, поэтому подключиться к дисплею терминал не мог. Подменять HOME терминалу и не требуется — это делает сценарий входа, уже внутри окна, перед самым запуском agy. Заодно устранено побочное действие в запросе: сценарий входа создавался внутри функции ПОИСКА терминала. Теперь его готовит start_native_agy_login, а поиск только отвечает на вопрос и ничего не создаёт. Владелец не мог понять, обновился ли хаб: на Linux строка сборки в боковой панели была пуста, потому что заполнялась из панели обновлений, а та подтягивается только при открытии. Теперь номер берётся из снапшота и показывается вместе со временем запуска процесса. Показывается running_commit — снятый ОДИН РАЗ при старте процесса. Поле commit читается из манифеста на диске при каждом запросе, поэтому переживший обновление процесс рапортует им свежий номер при старом поведении; на этом я уже спотыкался при разборе окон консоли. Снятое при старте значение отвечает на настоящий вопрос: какой код сейчас в памяти. 678 passed, 2 skipped; ruff чисто; релизный гейт пройден. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
884a632049
commit
28f35f863f
9 changed files with 131 additions and 35 deletions
|
|
@ -18,7 +18,7 @@ namespace HermesHubSetup
|
|||
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
|
||||
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
|
||||
// какого кода собран установщик.
|
||||
public const string BuildCommit = "6cb0d7c";
|
||||
public const string BuildCommit = "884a632";
|
||||
public const string MIN_HERMES_VERSION = "0.20.0";
|
||||
public const string MAX_TESTED_HERMES = "0.20.4";
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -870,9 +870,10 @@ def find_terminal_emulator(
|
|||
)
|
||||
return None, err_msg, checked
|
||||
|
||||
# Запускаем не саму agy, а сценарий: он сам задаёт HOME и не даёт окну
|
||||
# закрыться. Подробности — в write_login_helper.
|
||||
launch = str(write_login_helper(profile_dir, agy_exe, profile_id))
|
||||
# Что запускаем, решает вызывающий: поиск терминала не должен ничего
|
||||
# создавать. Нативный вход передаёт сюда путь сценария входа, а не саму
|
||||
# agy — см. write_login_helper.
|
||||
launch = agy_exe
|
||||
|
||||
# Конкретные эмуляторы идут раньше x-terminal-emulator: это обёртка над
|
||||
# альтернативами Debian, лишний слой между нами и настоящей программой.
|
||||
|
|
@ -1074,7 +1075,10 @@ def start_native_agy_login(
|
|||
except Exception as exc:
|
||||
return False, str(exc), {"profile_id": slot, "home": str(profile_dir)}
|
||||
|
||||
term_cmd, err_msg, checked = find_terminal_emulator(slot, agy_exe, profile_dir)
|
||||
# Терминал запускает сценарий, а не agy напрямую: сценарий сам задаёт HOME
|
||||
# и не даёт окну закрыться вместе с командой.
|
||||
launch_script = str(write_login_helper(profile_dir, agy_exe, slot))
|
||||
term_cmd, err_msg, checked = find_terminal_emulator(slot, launch_script, profile_dir)
|
||||
if err_msg or not term_cmd:
|
||||
return False, err_msg or "Терминал не найден", {
|
||||
"profile_id": slot,
|
||||
|
|
@ -1082,18 +1086,16 @@ def start_native_agy_login(
|
|||
"checked_terminals": checked,
|
||||
}
|
||||
|
||||
# Дисплей, найденный у systemd, обязан попасть в окружение терминала:
|
||||
# в окружении самого хаба его может не быть, если хаб запущен по SSH или
|
||||
# службой, а окно всё равно должно открыться на рабочем столе владельца.
|
||||
overrides = {
|
||||
"HOME": str(profile_dir),
|
||||
"USERPROFILE": str(profile_dir),
|
||||
"HOMEPATH": str(profile_dir),
|
||||
}
|
||||
# HOME терминалу НЕ подменяем. Клиенты X11 берут ключ авторизации из
|
||||
# ~/.Xauthority, и с подменённым HOME его там нет: xfce4-terminal не мог
|
||||
# подключиться к дисплею и выходил с кодом 1, не открыв окна. Изоляцию
|
||||
# обеспечивает сценарий входа — он задаёт HOME сам, уже внутри терминала,
|
||||
# непосредственно перед запуском agy.
|
||||
#
|
||||
# Дисплей, найденный у systemd, наоборот, обязан попасть в окружение: в
|
||||
# окружении самого хаба его может не быть, если хаб запущен по SSH.
|
||||
session_env, _checked = detect_graphical_session()
|
||||
overrides.update(session_env)
|
||||
|
||||
env = build_safe_subprocess_env(overrides=overrides)
|
||||
env = build_safe_subprocess_env(overrides=dict(session_env))
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
|
|
|
|||
|
|
@ -99,6 +99,19 @@ def get_auth_token(x_hub_token: str = Header(None)) -> bool:
|
|||
raise HTTPException(status_code=401, detail="Invalid X-Hub-Token")
|
||||
return True
|
||||
|
||||
# Коммит и время запуска СНИМАЮТСЯ ОДИН РАЗ, при старте процесса.
|
||||
#
|
||||
# get_installed_commit() читает манифест с диска при каждом вызове, поэтому
|
||||
# переживший обновление процесс бодро рапортует свежий коммит: владелец видит
|
||||
# новый номер и старое поведение. Отличить сборки по такому полю нельзя.
|
||||
#
|
||||
# Снятое при старте значение отвечает на настоящий вопрос: какой код сейчас
|
||||
# в памяти. Вместе со временем запуска этого достаточно, чтобы понять, дошло
|
||||
# ли обновление до работы, а не только до файлов.
|
||||
RUNNING_COMMIT: str = get_installed_commit()
|
||||
PROCESS_STARTED_AT: float = time.time()
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health_check():
|
||||
from ..account_probe_service import AccountProbeService
|
||||
|
|
@ -108,6 +121,8 @@ def health_check():
|
|||
"ok": True,
|
||||
"version": __version__,
|
||||
"commit": get_installed_commit(),
|
||||
"running_commit": RUNNING_COMMIT,
|
||||
"started_at": PROCESS_STARTED_AT,
|
||||
# Antigravity и Claude раньше стояли здесь как supported: False с
|
||||
# советом идти в десктоп или пробрасывать порты. Это было неверно:
|
||||
# ProfileOAuthSession.handle_manual_callback_url и
|
||||
|
|
@ -236,6 +251,8 @@ def get_snapshot(authorized: bool = Depends(get_auth_token)):
|
|||
|
||||
snap_dict["version"] = __version__
|
||||
snap_dict["commit"] = get_installed_commit()
|
||||
snap_dict["running_commit"] = RUNNING_COMMIT
|
||||
snap_dict["started_at"] = PROCESS_STARTED_AT
|
||||
snap_dict["system_paths"] = {
|
||||
"hermes_home": str(paths.get_hermes_home()),
|
||||
"config_dir": str(paths.get_config_dir()),
|
||||
|
|
|
|||
|
|
@ -2426,12 +2426,30 @@ function renderUpdateUI() {
|
|||
const badgeText = document.getElementById('header-update-text');
|
||||
const commitTag = document.getElementById('commit-tag');
|
||||
|
||||
const installedCommit = (latestUpdateInfo && latestUpdateInfo.installed_commit && latestUpdateInfo.installed_commit !== 'unknown')
|
||||
? latestUpdateInfo.installed_commit
|
||||
: (currentSettings && currentSettings.installed_commit ? currentSettings.installed_commit : '');
|
||||
// Первым источником — снапшот работающего сервера: он приходит всегда, а
|
||||
// панель обновлений заполняется только при её открытии. На Linux строка
|
||||
// сборки поэтому оставалась пустой, и понять, дошло ли обновление, было
|
||||
// нельзя.
|
||||
//
|
||||
// Берём running_commit — коммит, снятый при СТАРТЕ процесса. Поле commit
|
||||
// читается с диска при каждом запросе, и переживший обновление процесс
|
||||
// рапортует им свежий номер при старом поведении.
|
||||
const runningCommit = (currentSnapshot && currentSnapshot.running_commit) || '';
|
||||
const installedCommit = runningCommit
|
||||
|| (latestUpdateInfo && latestUpdateInfo.installed_commit && latestUpdateInfo.installed_commit !== 'unknown'
|
||||
? latestUpdateInfo.installed_commit
|
||||
: (currentSettings && currentSettings.installed_commit ? currentSettings.installed_commit : ''));
|
||||
|
||||
if (commitTag) {
|
||||
commitTag.textContent = installedCommit ? `Сборка: ${installedCommit.slice(0, 7)}` : 'Сборка: —';
|
||||
if (!installedCommit) {
|
||||
commitTag.textContent = 'Сборка: Н/Д (сервер не передал номер)';
|
||||
} else {
|
||||
const startedAt = currentSnapshot && currentSnapshot.started_at;
|
||||
const since = startedAt
|
||||
? ` · запущен ${new Date(startedAt * 1000).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}`
|
||||
: '';
|
||||
commitTag.textContent = `Сборка: ${installedCommit.slice(0, 7)}${since}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (badge && badgeText) {
|
||||
|
|
|
|||
|
|
@ -74,14 +74,9 @@ def test_terminal_discovery_linux_found(tmp_path, monkeypatch):
|
|||
assert cmd is not None
|
||||
assert cmd[0] == "/usr/bin/gnome-terminal"
|
||||
assert "--" in cmd
|
||||
# Терминалу передаётся не сама agy, а сценарий: он задаёт HOME сам и
|
||||
# не даёт окну закрыться. Многие эмуляторы передают вызов уже
|
||||
# работающему экземпляру, и наследование окружения теряется.
|
||||
helper = tmp_path / ".hermes-agy-login.sh"
|
||||
assert str(helper) in cmd
|
||||
body = helper.read_text(encoding="utf-8")
|
||||
assert "/bin/agy" in body
|
||||
assert str(tmp_path) in body
|
||||
# Поиск терминала подставляет то, что ему передали, и ничего не создаёт.
|
||||
# Сценарий входа готовит start_native_agy_login.
|
||||
assert "/bin/agy" in cmd
|
||||
assert any("gnome-terminal (найден: /usr/bin/gnome-terminal)" in item for item in checked)
|
||||
|
||||
|
||||
|
|
@ -167,13 +162,20 @@ def test_start_native_agy_login_creates_isolated_home(tmp_path, monkeypatch):
|
|||
assert "session_id" in data
|
||||
assert pdir.is_dir()
|
||||
|
||||
# Verify subprocess called with isolated HOME
|
||||
# Изоляция обеспечивается сценарием входа, а не окружением терминала.
|
||||
# HOME терминалу подменять нельзя: клиенты X11 берут ключ авторизации из
|
||||
# ~/.Xauthority, и с подменённым HOME его там нет — xfce4-terminal выходил
|
||||
# с кодом 1, не открыв окна. Сценарий задаёт HOME сам, уже внутри окна.
|
||||
mock_popen.assert_called_once()
|
||||
call_kwargs = mock_popen.call_args[1]
|
||||
assert call_kwargs["env"]["HOME"] == str(pdir)
|
||||
assert call_kwargs["env"]["USERPROFILE"] == str(pdir)
|
||||
assert call_kwargs["env"].get("HOME") != str(pdir)
|
||||
assert call_kwargs["cwd"] == str(pdir)
|
||||
|
||||
helper = pdir / ".hermes-agy-login.sh"
|
||||
assert helper.is_file(), "сценарий входа должен быть создан"
|
||||
body = helper.read_text(encoding="utf-8")
|
||||
assert str(pdir) in body and "export HOME" in body
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_start_native_agy_login_occupied_slot_requires_confirmation(tmp_path, monkeypatch):
|
||||
|
|
|
|||
|
|
@ -94,14 +94,15 @@ def test_alternatives_wrapper_is_the_last_resort(linux, monkeypatch, tmp_path):
|
|||
assert cmd[0] == "/usr/bin/xterm"
|
||||
|
||||
|
||||
def test_terminal_runs_the_helper_not_agy_directly(linux, monkeypatch, tmp_path):
|
||||
def test_terminal_discovery_creates_nothing(linux, monkeypatch, tmp_path):
|
||||
"""Поиск терминала — запрос, а не действие: файлов после него не остаётся."""
|
||||
monkeypatch.setattr(shutil, "which", _only({"xterm"}))
|
||||
|
||||
cmd, err, _ = find_terminal_emulator("ag-6", "/usr/local/bin/agy", tmp_path)
|
||||
cmd, err, _ = find_terminal_emulator("ag-6", "/путь/к/запуску", tmp_path)
|
||||
|
||||
assert err is None
|
||||
assert "/usr/local/bin/agy" not in cmd
|
||||
assert str(tmp_path / ".hermes-agy-login.sh") in cmd
|
||||
assert "/путь/к/запуску" in cmd
|
||||
assert not (tmp_path / ".hermes-agy-login.sh").exists()
|
||||
|
||||
|
||||
def test_missing_terminal_lists_what_was_checked(linux, monkeypatch, tmp_path):
|
||||
|
|
|
|||
56
tests/test_running_build_is_visible.py
Normal file
56
tests/test_running_build_is_visible.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Владелец должен видеть, какой код сейчас работает.
|
||||
|
||||
На Linux строка сборки в боковой панели оставалась пустой: она заполнялась из
|
||||
панели обновлений, а та подтягивается только при открытии. Понять, дошло ли
|
||||
обновление, было нельзя.
|
||||
|
||||
Глубже: поле commit читается из манифеста на диске при каждом запросе, поэтому
|
||||
переживший обновление процесс рапортует свежий номер при старом поведении.
|
||||
Отличить сборки по нему невозможно — это уже подводило при разборе окон
|
||||
консоли. running_commit снимается один раз, при старте процесса, и отвечает на
|
||||
настоящий вопрос: какой код в памяти.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from antigravity_provider.router.web.server import (
|
||||
PROCESS_STARTED_AT,
|
||||
RUNNING_COMMIT,
|
||||
app,
|
||||
)
|
||||
|
||||
|
||||
def test_health_reports_the_running_build():
|
||||
payload = TestClient(app).get("/api/health").json()
|
||||
|
||||
assert payload["running_commit"] == RUNNING_COMMIT
|
||||
assert payload["started_at"] == PROCESS_STARTED_AT
|
||||
|
||||
|
||||
def test_snapshot_reports_the_running_build():
|
||||
snap = TestClient(app).get("/api/snapshot").json()
|
||||
|
||||
assert snap["running_commit"] == RUNNING_COMMIT
|
||||
assert snap["started_at"] == PROCESS_STARTED_AT
|
||||
|
||||
|
||||
def test_running_commit_is_taken_once_and_does_not_follow_the_disk(monkeypatch):
|
||||
"""Подмена манифеста на диске не должна менять номер работающей сборки."""
|
||||
from antigravity_provider.router.web import server
|
||||
|
||||
monkeypatch.setattr(server, "get_installed_commit", lambda: "deadbee")
|
||||
payload = TestClient(app).get("/api/health").json()
|
||||
|
||||
assert payload["commit"] == "deadbee", "поле commit отражает диск"
|
||||
assert payload["running_commit"] == RUNNING_COMMIT, (
|
||||
"а running_commit — то, что реально запущено"
|
||||
)
|
||||
assert payload["running_commit"] != "deadbee"
|
||||
|
||||
|
||||
def test_start_time_is_plausible():
|
||||
assert PROCESS_STARTED_AT > 0
|
||||
assert PROCESS_STARTED_AT <= time.time()
|
||||
Loading…
Reference in a new issue