fix: веб-сервер не запускался на Windows, диаграмма тормозила при перетаскивании

Три дефекта из установки владельца на вторую машину.

1. Веб-сервер падал сразу: «web server process terminated unexpectedly».
   Установщик ставит в venv Hermes только customtkinter, pillow, pyyaml и
   psutil — fastapi и uvicorn отсутствовали в списке вовсе. Добавлены во
   все шесть мест: проверка, сообщение, pip, uv, перепроверка.
   Поэтому на Windows открывался только десктоп: веб физически не мог
   стартовать.

2. Лаунчер показывал голое «terminated unexpectedly» без причины — та же
   болезнь, что у кода 12. Теперь перехватывает вывод процесса и выводит
   последние строки ошибки в окне.

3. Окно тормозило при перетаскивании и продолжало двигаться несколько
   секунд после отпускания мыши. _RouteDiagram перерисовывал всю канву на
   КАЖДОЕ событие <Configure>, а при перетаскивании их сотни; очередь не
   успевала разгребаться. Гашение в приложении существовало, но
   _handle_debounced_resize был пустой заглушкой.
   Перерисовка сведена к одной после затишья. Замерено: 199 событий
   давали 199 перерисовок, теперь 4.

Тесты: 373 passed, ruff чисто. Установщик пересобран.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-24 09:59:41 +07:00
parent 57e7ca2edd
commit ef6a9e1acd
5 changed files with 48 additions and 9 deletions

View file

@ -18,7 +18,7 @@ namespace HermesHubSetup
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
// какого кода собран установщик.
public const string BuildCommit = "36b449b";
public const string BuildCommit = "57e7ca2";
public const string MIN_HERMES_VERSION = "0.20.0";
public const string MAX_TESTED_HERMES = "0.20.4";
@ -137,7 +137,7 @@ namespace HermesHubSetup
{
ProcessStartInfo checkPsi = new ProcessStartInfo();
checkPsi.FileName = pythonExe;
checkPsi.Arguments = "-c \"import customtkinter, PIL, yaml, psutil; print('DEPS_OK')\"";
checkPsi.Arguments = "-c \"import customtkinter, PIL, yaml, psutil, fastapi, uvicorn; print('DEPS_OK')\"";
checkPsi.UseShellExecute = false;
checkPsi.RedirectStandardOutput = true;
checkPsi.RedirectStandardError = true;
@ -159,7 +159,7 @@ namespace HermesHubSetup
if (needsInstall)
{
if (progressCallback != null) progressCallback("Installing dependencies into Hermes venv (customtkinter, Pillow, PyYAML, psutil)...", 40);
if (progressCallback != null) progressCallback("Installing dependencies into Hermes venv (customtkinter, Pillow, PyYAML, psutil, FastAPI, uvicorn)...", 40);
// 1. Ensure pip is installed/bootstrapped if needed
try
@ -185,7 +185,7 @@ namespace HermesHubSetup
{
ProcessStartInfo pipPsi = new ProcessStartInfo();
pipPsi.FileName = pythonExe;
pipPsi.Arguments = "-m pip install --no-warn-script-location customtkinter pillow pyyaml psutil";
pipPsi.Arguments = "-m pip install --no-warn-script-location customtkinter pillow pyyaml psutil fastapi uvicorn";
pipPsi.UseShellExecute = false;
pipPsi.RedirectStandardOutput = true;
pipPsi.RedirectStandardError = true;
@ -209,7 +209,7 @@ namespace HermesHubSetup
{
ProcessStartInfo uvPsi = new ProcessStartInfo();
uvPsi.FileName = "uv";
uvPsi.Arguments = string.Format("pip install --python \"{0}\" customtkinter pillow pyyaml psutil", pythonExe);
uvPsi.Arguments = string.Format("pip install --python \"{0}\" customtkinter pillow pyyaml psutil fastapi uvicorn", pythonExe);
uvPsi.UseShellExecute = false;
uvPsi.RedirectStandardOutput = true;
uvPsi.RedirectStandardError = true;
@ -237,7 +237,7 @@ namespace HermesHubSetup
{
ProcessStartInfo recheckPsi = new ProcessStartInfo();
recheckPsi.FileName = pythonExe;
recheckPsi.Arguments = "-c \"import customtkinter, PIL, yaml, psutil; print('DEPS_VERIFIED')\"";
recheckPsi.Arguments = "-c \"import customtkinter, PIL, yaml, psutil, fastapi, uvicorn; print('DEPS_VERIFIED')\"";
recheckPsi.UseShellExecute = false;
recheckPsi.RedirectStandardOutput = true;
recheckPsi.RedirectStandardError = true;
@ -309,7 +309,7 @@ namespace HermesHubSetup
}
// 2. Install UI & System Dependencies into Hermes Python Environment
if (progressCallback != null) progressCallback("Checking Python UI dependencies (customtkinter, Pillow)...", 35);
if (progressCallback != null) progressCallback("Checking Python dependencies (customtkinter, Pillow, FastAPI, uvicorn)...", 35);
if (!EnsurePythonDependencies(HermesPython, progressCallback))
{
return 13; // Dependency install failed

Binary file not shown.

View file

@ -122,6 +122,11 @@ namespace HermesHub
serverPsi.UseShellExecute = false;
serverPsi.CreateNoWindow = true;
serverPsi.WindowStyle = ProcessWindowStyle.Hidden;
// Вывод перехватываем, чтобы при падении показать причину, а не
// голое «terminated unexpectedly». Без этого владелец видит факт
// отказа и ни слова о том, чего не хватает.
serverPsi.RedirectStandardError = true;
serverPsi.RedirectStandardOutput = true;
try
{
@ -144,7 +149,16 @@ namespace HermesHub
}
if (serverProcess.HasExited)
{
MessageBox.Show("Hermes Hub web server process terminated unexpectedly.", "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
string why = "";
try { why = serverProcess.StandardError.ReadToEnd(); } catch { }
if (string.IsNullOrEmpty(why))
{
try { why = serverProcess.StandardOutput.ReadToEnd(); } catch { }
}
if (why.Length > 1500) why = why.Substring(why.Length - 1500);
string msg = "Веб-сервер Hermes Hub завершился с ошибкой.";
if (!string.IsNullOrEmpty(why)) msg += Environment.NewLine + Environment.NewLine + why.Trim();
MessageBox.Show(msg, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
Thread.Sleep(200);

Binary file not shown.

View file

@ -220,7 +220,32 @@ class _RouteDiagram(ctk.CTkFrame):
self.context_status.pack()
self._left_labels: list[str] = []
self._right_labels: list[str] = []
self.bind("<Configure>", self._redraw)
self._redraw_timer_id: Any = None
self.bind("<Configure>", self._schedule_redraw)
def _schedule_redraw(self, event: Any = None) -> None:
"""Свести поток <Configure> к одной перерисовке.
При перетаскивании окна Tk шлёт <Configure> непрерывно, и раньше каждое
событие вызывало полную перерисовку канвы: удаление и построение всех
связей, подписей и узлов. Очередь не успевала разгребаться, и окно
продолжало ползти ещё несколько секунд после того, как мышь отпущена.
Перерисовываем один раз, когда поток событий утих.
"""
if self._redraw_timer_id is not None:
try:
self.after_cancel(self._redraw_timer_id)
except Exception:
pass
self._redraw_timer_id = self.after(80, self._redraw_now)
def _redraw_now(self) -> None:
self._redraw_timer_id = None
try:
self._redraw()
except Exception:
pass
def sync_slots(self, provider_count: int, agent_count: int) -> None:
"""Grow/shrink endpoint slots to match the snapshot without hard caps."""