Auto-restart gateway after Telegram QR onboarding

This commit is contained in:
Shannon Sands 2026-06-10 16:43:50 +10:00 committed by Teknium
parent 298bb93d39
commit 984e69ff62
4 changed files with 124 additions and 10 deletions

View file

@ -3748,6 +3748,29 @@ async def get_telegram_onboarding_status(pairing_id: str):
)
def _restart_gateway_after_telegram_onboarding() -> dict[str, Any]:
"""Best-effort gateway restart after saving Telegram QR onboarding.
The QR flow naturally pulls users into Telegram on another device. If the
saved token waits on a separate dashboard restart click, Hermes appears
broken from the chat side. Keep the config save authoritative, but report
restart failures so the UI can fall back to the existing manual banner.
"""
try:
proc = _spawn_hermes_action(["gateway", "restart"], "gateway-restart")
except Exception as exc:
_log.exception("Failed to auto-restart gateway after Telegram onboarding")
return {
"restart_started": False,
"restart_error": str(exc),
}
return {
"restart_started": True,
"restart_action": "gateway-restart",
"restart_pid": proc.pid,
}
@app.post("/api/messaging/telegram/onboarding/{pairing_id}/apply")
async def apply_telegram_onboarding(
pairing_id: str, body: TelegramOnboardingApply
@ -3802,11 +3825,14 @@ async def apply_telegram_onboarding(
with _telegram_onboarding_lock:
_telegram_onboarding_pairings.pop(pairing_id, None)
restart_result = _restart_gateway_after_telegram_onboarding()
return {
"ok": True,
"platform": "telegram",
"bot_username": bot_username,
"needs_restart": True,
"needs_restart": not restart_result["restart_started"],
**restart_result,
}

View file

@ -1399,6 +1399,16 @@ class TestWebServerEndpoints:
}
monkeypatch.setattr(ws, "_telegram_onboarding_request_sync", fake_request)
restart_calls = []
class FakeRestartProc:
pid = 4242
def fake_spawn_action(subcommand, name):
restart_calls.append((subcommand, name))
return FakeRestartProc()
monkeypatch.setattr(ws, "_spawn_hermes_action", fake_spawn_action)
start = self.client.post("/api/messaging/telegram/onboarding/start", json={})
assert start.status_code == 200
@ -1420,8 +1430,73 @@ class TestWebServerEndpoints:
"ok": True,
"platform": "telegram",
"bot_username": "hermes_pair_ready_bot",
"needs_restart": True,
"needs_restart": False,
"restart_started": True,
"restart_action": "gateway-restart",
"restart_pid": 4242,
}
assert restart_calls == [(["gateway", "restart"], "gateway-restart")]
env = load_env()
assert env["TELEGRAM_BOT_TOKEN"] == "123456:SECRET"
assert env["TELEGRAM_ALLOWED_USERS"] == "123456789"
assert load_config()["platforms"]["telegram"]["enabled"] is True
def test_telegram_onboarding_apply_reports_restart_failure_after_save(
self, monkeypatch
):
import hermes_cli.web_server as ws
from hermes_cli.config import load_config, load_env
with ws._telegram_onboarding_lock:
ws._telegram_onboarding_pairings.clear()
def fake_request(method, path, *, body=None, bearer_token=None):
if method == "POST":
return {
"pairing_id": "pair-restart-fails",
"poll_token": "poll-secret",
"suggested_username": "hermes_pair_restart_fails_bot",
"deep_link": "https://t.me/newbot/HermesSetupBot/hermes_pair_restart_fails_bot",
"qr_payload": "https://t.me/newbot/HermesSetupBot/hermes_pair_restart_fails_bot",
"expires_at": "2027-05-18T00:00:00.000Z",
}
assert method == "GET"
assert path == "/v1/telegram/pairings/pair-restart-fails"
assert bearer_token == "poll-secret"
return {
"status": "ready",
"bot_username": "hermes_pair_restart_fails_bot",
"owner_user_id": 123456789,
"token": "123456:SECRET",
}
monkeypatch.setattr(ws, "_telegram_onboarding_request_sync", fake_request)
def fail_spawn_action(subcommand, name):
assert subcommand == ["gateway", "restart"]
assert name == "gateway-restart"
raise RuntimeError("supervisor unavailable")
monkeypatch.setattr(ws, "_spawn_hermes_action", fail_spawn_action)
start = self.client.post("/api/messaging/telegram/onboarding/start", json={})
assert start.status_code == 200
ready = self.client.get("/api/messaging/telegram/onboarding/pair-restart-fails")
assert ready.status_code == 200
assert ready.json()["status"] == "ready"
applied = self.client.post(
"/api/messaging/telegram/onboarding/pair-restart-fails/apply",
json={"allowed_user_ids": ["123456789"]},
)
assert applied.status_code == 200
applied_data = applied.json()
assert applied_data["ok"] is True
assert applied_data["needs_restart"] is True
assert applied_data["restart_started"] is False
assert "supervisor unavailable" in applied_data["restart_error"]
assert "token" not in applied_data
env = load_env()
assert env["TELEGRAM_BOT_TOKEN"] == "123456:SECRET"
assert env["TELEGRAM_ALLOWED_USERS"] == "123456789"

View file

@ -1475,7 +1475,11 @@ export interface TelegramOnboardingApplyResponse {
ok: boolean;
platform: "telegram";
bot_username?: string;
needs_restart: true;
needs_restart: boolean;
restart_started?: boolean;
restart_action?: string;
restart_pid?: number | null;
restart_error?: string;
}
export interface SessionMessage {

View file

@ -608,19 +608,28 @@ function TelegramOnboardingPanel({
setPhase("applying");
setError("");
try {
await api.applyTelegramOnboarding(setup.pairing_id, {
const result = await api.applyTelegramOnboarding(setup.pairing_id, {
allowed_user_ids: allowedIds,
});
resetSetup();
showToast("Telegram saved", "success");
try {
await api.restartGateway();
showToast("Gateway restarting…", "success");
if (result.restart_started) {
showToast("Telegram saved; gateway restarting…", "success");
setRestartNeeded(false);
setTimeout(() => void onChanged(), 4000);
} catch (restartError) {
} else if (result.restart_started === undefined && result.needs_restart) {
try {
await api.restartGateway();
showToast("Telegram saved; gateway restarting…", "success");
setRestartNeeded(false);
setTimeout(() => void onChanged(), 4000);
} catch (restartError) {
onRestartNeeded();
showToast(`Telegram saved; gateway restart failed: ${restartError}`, "error");
}
} else {
onRestartNeeded();
showToast(`Telegram saved; restart failed: ${restartError}`, "error");
const detail = result.restart_error ? `: ${result.restart_error}` : "";
showToast(`Telegram saved; gateway restart failed${detail}`, "error");
}
await onChanged();
} catch (applyError) {