Files
server-manager/plans/test-rdp.md
chrome-storm-c442 142b68515c v1.8.36: fix fullscreen toggle — _is_detached flag prevents re-embed conflict
- Added _is_detached flag to EmbeddedRDP (set in detach(), cleared in reattach())
- _monitor_tick() skips try_reembed() when window is intentionally detached
- Prevents race condition: monitor tick no longer re-embeds fullscreen window
- Added plans/test-rdp.md — RDP testing documentation
- build.py auto-cleanup removed v1.8.31

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:59:16 -05:00

454 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ручное тестирование Embedded RDP
Практическое руководство по тестированию встроенного RDP-клиента в ServerManager.
## Общие сведения
- **Платформа:** только Windows (на Linux/macOS — fallback на внешний клиент)
- **Лог-файл:** `~/.server-connections/app.log` (ротация 5 МБ, 3 бэкапа)
- **Формат логов:** `YYYY-MM-DD HH:MM:SS [LEVEL] servermanager: message`
- **Ключевые файлы:**
- `core/remote_desktop.py` — класс `EmbeddedRDP`, Win32 embedding
- `gui/tabs/launch_tab.py` — UI таба Launch, управление состоянием
- `core/logger.py` — настройки логгера
---
## Тест 1: Первичное подключение и встраивание
### Предусловия
- В ServerManager добавлен сервер типа `rdp` с валидными credentials
- Сервер доступен по сети (порт 3389 открыт)
- Запущен `python main.py` или exe из `releases/`
### Шаги
1. Выбрать RDP-сервер в боковой панели (sidebar)
2. Перейти на вкладку **Launch**
3. Убедиться, что видна панель настроек: Quality, Clipboard, Drives, Printers
4. Оставить настройки по умолчанию (Auto, Clipboard ON, остальное OFF)
5. Нажать кнопку **Connect**
### Ожидаемый результат
- Кнопка Connect блокируется (state=disabled)
- Статус меняется на "Connecting..." (жёлтый цвет)
- Панель настроек скрывается, появляется тулбар с кнопками Disconnect/Fullscreen
- В тулбаре статус "Embedding..." (жёлтый)
- Через 1-5 секунд окно mstsc.exe встраивается в чёрный фрейм
- Статус меняется на "Connected: {alias}" (зелёный)
- Окно mstsc заполняет весь доступный фрейм
### Что проверить в логах
```
grep -i "mstsc\|rdp\|embed\|SetParent\|AttachThread" ~/.server-connections/app.log
```
Должны быть строки:
- `Embedded RDP file: C:\...\sm_embed_{alias}.rdp` — .rdp файл создан
- `RDP server pre-trusted in registry: {host}` — сертификат доверен
- `mstsc.exe launched, PID=XXXX` — процесс запущен
- `Embed attempt N: found M mstsc windows` — окно найдено
- `HWND=XXXX class='TscShellContainerClass'` — правильный класс окна
- `AttachThreadInput(our, target): True` — потоки привязаны
- `SetParent(hwnd=XXXX, parent=YYYY) = ZZZZ` — встраивание выполнено
- `mstsc embedded: HWND=XXXX into parent=YYYY` — успех
### Частые проблемы
| Симптом | Причина | Решение |
|---------|---------|---------|
| Окно mstsc открылось отдельно, не встроилось | `SetParent` вернул 0 | Проверить `GetLastError` в логах, возможно UIPI (запуск от другого пользователя) |
| Появился диалог сертификата | Сервер не в реестре | `_trust_rdp_server` должен был подавить, проверить реестр `HKCU\Software\Microsoft\Terminal Server Client` |
| Timeout 20 секунд | mstsc не стартовал | Проверить что mstsc.exe доступен в PATH, нет ли блокировки антивирусом |
| Встроился диалог вместо основного окна | Фаза 1 нашла только `#32770` | Фаза 2 должна дождаться `TscShellContainerClass` (до 30 сек) |
---
## Тест 2: Fullscreen (detach / reattach)
### Предусловия
- RDP-сессия активна и встроена (тест 1 пройден)
- Статус "Connected" (зелёный)
### Шаги
1. Нажать кнопку **Fullscreen** в тулбаре
2. Убедиться что окно mstsc развернулось на весь экран
3. Поработать в RDP-сессии (открыть Explorer, cmd и т.п.)
4. Переключиться обратно в ServerManager (Alt+Tab)
5. Нажать кнопку **Exit Fullscreen** (она заменила Fullscreen)
### Ожидаемый результат
- **При нажатии Fullscreen:**
- Кнопка меняет текст на "Exit Fullscreen"
- Окно mstsc отсоединяется от фрейма (`SetParent(hwnd, 0)`)
- Стиль окна восстанавливается до `WS_OVERLAPPEDWINDOW`
- Окно максимизируется (`SW_MAXIMIZE`)
- Фокус на mstsc (`SetForegroundWindow`)
- **При нажатии Exit Fullscreen:**
- Окно снова встраивается в фрейм (вызов `_embed`)
- Кнопка возвращает текст "Fullscreen"
- Размер окна подгоняется под фрейм
### Что проверить в логах
```
mstsc detached to fullscreen
mstsc reattached
AttachThreadInput(...)
SetParent(hwnd=XXXX, parent=YYYY)
```
### Частые проблемы
| Симптом | Причина | Решение |
|---------|---------|---------|
| После reattach чёрный экран | mstsc потерял rendering | Кликнуть в фрейм, фокус восстановит отрисовку |
| Окно mstsc не максимизируется | `ShowWindow(hwnd, SW_MAXIMIZE)` не сработал | Проверить что detach убрал `WS_CHILD` перед `SetParent(0)` |
| После Exit Fullscreen мониторинг не видит окно | `is_embedded()` возвращает False | `try_reembed` должен подхватить |
---
## Тест 3: Reconnect recovery (имитация разрыва сети)
### Предусловия
- RDP-сессия активна и встроена
- Доступ к сетевым настройкам (для имитации разрыва)
### Шаги
1. При активной RDP-сессии отключить сеть (Wi-Fi off, или вытащить кабель)
2. Подождать 5-15 секунд — mstsc покажет "Reconnecting..."
3. Включить сеть обратно
4. Подождать до 30 секунд пока mstsc переподключится
### Ожидаемый результат
- mstsc автоматически переподключается (параметр `autoreconnection enabled:i:1` в .rdp)
- При переподключении mstsc может создать новое окно или перепривязать HWND
- Мониторинг (`_monitor_tick`, каждые 500ms) детектирует потерю HWND:
- `is_embedded()` возвращает False (HWND перепривязан к десктопу)
- Вызывается `try_reembed()`
- Окно автоматически повторно встраивается в фрейм
- Статус меняется на "Reconnected: {alias}" (зелёный)
### Что проверить в логах
```
RDP window lost, attempting re-embed...
Re-embed: same HWND=XXXX lost parent (parent=0, expected=YYYY)
# или
Re-embed: found new mstsc window HWND=ZZZZ
RDP auto-recovered after reconnect
```
### Сценарии `try_reembed`
1. **Тот же HWND, другой parent** — mstsc reconnect сбросил parent на desktop → `_embed()` повторно
2. **Новый HWND** — mstsc создал новое окно → `EnumWindows` находит по PID или hostname в title
### Частые проблемы
| Симптом | Причина | Решение |
|---------|---------|---------|
| mstsc закрылся совсем | Сервер разорвал сессию | `is_alive()` вернёт False, `_on_rdp_exited()` покажет панель настроек |
| Повторное встраивание не сработало | Новое окно ещё не появилось | Мониторинг продолжит попытки каждые 500ms пока процесс жив |
| Зависание мониторинга | `after()` callback не вызвался | Проверить что GUI main loop не заблокирован |
---
## Тест 4: Disconnect
### Предусловия
- RDP-сессия активна
### Шаги
1. Нажать красную кнопку **Disconnect** в тулбаре
2. Дождаться возврата на панель настроек
### Ожидаемый результат
- Мониторинг останавливается (`_stop_monitor`)
- Если был fullscreen — сбрасывается флаг `_is_fullscreen`
- `EmbeddedRDP.disconnect()` вызывается:
- Все mstsc-процессы (parent + children) убиваются через psutil
- Основной процесс terminate → wait(3s) → kill
- Временный .rdp файл удаляется
- Тулбар и фрейм скрываются
- Панель настроек показывается
- Статус "Disconnected" (серый)
- Кнопка Connect разблокируется
### Что проверить в логах
```
EmbeddedRDP disconnected
```
### Частые проблемы
| Симптом | Причина | Решение |
|---------|---------|---------|
| mstsc процесс остался висеть | psutil не нашёл children | Проверить Task Manager, вручную убить |
| Файл sm_embed_*.rdp не удалён | Файл заблокирован mstsc | Удалится при следующем подключении (перезапись) |
---
## Тест 5: Переключение сервера при активном RDP
### Предусловия
- RDP-сессия активна к серверу A
- В sidebar есть второй сервер B (любого типа)
### Шаги
1. При активной RDP-сессии кликнуть на другой сервер в sidebar
2. Наблюдать за поведением
### Ожидаемый результат
- `set_server()` вызывается с новым alias
- Текущая RDP-сессия автоматически отключается (`_disconnect()`)
- mstsc убивается, фрейм очищается
- Загружается информация о новом сервере
- Если новый сервер тоже RDP — показываются его настройки и кнопка Connect
- Если новый сервер другого типа — таб Launch может скрыться (зависит от TAB_REGISTRY)
### Что проверить в логах
```
EmbeddedRDP disconnected
# Далее — логи для нового сервера если подключиться
```
### Частые проблемы
| Симптом | Причина | Решение |
|---------|---------|---------|
| Старая сессия не отключилась | `_disconnect()` не вызвался | Баг в `set_server()`, проверить что `self._embedded_rdp` был не None |
| Crash при быстром переключении | Race condition в мониторинге | `_stop_monitor()` должен отменить `after()` callback |
---
## Тест 6: Resize окна при активной сессии
### Предусловия
- RDP-сессия активна и встроена
### Шаги
1. Изменить размер окна ServerManager (потянуть за край)
2. Быстро менять размер несколько раз подряд
3. Максимизировать окно ServerManager
4. Восстановить размер окна
### Ожидаемый результат
- При изменении размера фрейма срабатывает `<Configure>` event
- Debounce 100ms — `MoveWindow` вызывается не чаще раза в 100ms
- Окно mstsc масштабируется (`smart sizing:i:1` в .rdp)
- Содержимое RDP-сессии растягивается под новый размер
### Что проверить в логах
- Обычно resize не логируется (слишком частая операция)
- Проверить что нет ошибок `Exception` в логах после resize
### Частые проблемы
| Симптом | Причина | Решение |
|---------|---------|---------|
| Мерцание при быстром resize | Слишком частые `MoveWindow` | Debounce 100ms должен помогать, можно увеличить до 150-200ms |
| Чёрные полосы по краям | `MoveWindow` размеры не совпадают с фреймом | Проверить что `winfo_width/height` возвращает правильные значения |
| При fullscreen resize не работает | `_on_rdp_resize` выходит при `_is_fullscreen=True` | Это правильное поведение — в fullscreen mstsc сам управляет размером |
---
## Автоматическое тестирование (auto_test подход)
### Принцип работы
Автоматический тест GUI использует Win32 API для программного управления окном ServerManager:
- Поиск окна SM по заголовку/классу через `EnumWindows`
- Клик по элементам интерфейса через `SendMessage` / `mouse_event`
- Скриншоты для верификации через `BitBlt` или PIL
### Маппинг координат (DPI scaling)
На системах с высоким DPI (2x масштабирование) необходимо учитывать разницу между логическими и физическими пикселями:
```python
# Получение DPI scale factor
import ctypes
user32 = ctypes.windll.user32
user32.SetProcessDPIAware() # Важно! Без этого координаты будут неверны
# Логические координаты (возвращает tkinter/winfo):
# x=100, y=200
# Физические координаты (нужны для mouse_event/SetCursorPos):
# x=200, y=400 (при 2x scaling)
# Формула:
dpi = user32.GetDpiForWindow(hwnd) # 192 для 2x
scale = dpi / 96 # 2.0
phys_x = int(log_x * scale)
phys_y = int(log_y * scale)
```
На данной системе (Windows 10 Pro for Workstations) используется 2x логический масштаб. Все координаты для `SetCursorPos` / `mouse_event` нужно умножать на 2.
### Поиск окна ServerManager
```python
import ctypes
import ctypes.wintypes
user32 = ctypes.windll.user32
WNDENUMPROC = ctypes.WINFUNCTYPE(
ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM
)
sm_hwnd = None
def find_sm(hwnd, _):
global sm_hwnd
buf = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(hwnd, buf, 256)
if "ServerManager" in buf.value or "Server Manager" in buf.value:
sm_hwnd = hwnd
return False
return True
user32.EnumWindows(WNDENUMPROC(find_sm), 0)
```
### Клик по элементам
```python
import ctypes
def click_at(x, y):
"""Клик по абсолютным координатам экрана (физическим)."""
ctypes.windll.user32.SetCursorPos(x, y)
# Left button down + up
ctypes.windll.user32.mouse_event(0x0002, 0, 0, 0, 0) # MOUSEEVENTF_LEFTDOWN
ctypes.windll.user32.mouse_event(0x0004, 0, 0, 0, 0) # MOUSEEVENTF_LEFTUP
def click_in_window(hwnd, rel_x, rel_y, scale=2.0):
"""Клик относительно окна (логические координаты)."""
rect = ctypes.wintypes.RECT()
ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect))
abs_x = rect.left + int(rel_x * scale)
abs_y = rect.top + int(rel_y * scale)
click_at(abs_x, abs_y)
```
### Очистка (atexit + PowerShell wildcard kill)
Автотесты должны гарантировать что после завершения (в т.ч. аварийного) не останется зависших процессов:
```python
import atexit
import subprocess
def cleanup():
"""Убить все связанные процессы при выходе."""
# Убить все mstsc процессы, запущенные тестом
subprocess.run(
["powershell", "-Command",
"Get-Process mstsc -ErrorAction SilentlyContinue | Stop-Process -Force"],
capture_output=True
)
# Убить ServerManager если запущен тестом
subprocess.run(
["powershell", "-Command",
"Get-Process -Name 'ServerManager*' -ErrorAction SilentlyContinue | Stop-Process -Force"],
capture_output=True
)
# Удалить временные .rdp файлы
subprocess.run(
["powershell", "-Command",
"Remove-Item $env:TEMP\\sm_embed_*.rdp -Force -ErrorAction SilentlyContinue"],
capture_output=True
)
atexit.register(cleanup)
```
PowerShell wildcard `ServerManager*` ловит и `ServerManager.exe`, и `ServerManager-vX.Y.Z-win-x64.exe`.
### Структура автотеста
```python
import time
import atexit
import subprocess
atexit.register(cleanup)
# 1. Запустить SM
proc = subprocess.Popen(["python", "main.py"])
time.sleep(3) # Дождаться загрузки GUI
# 2. Найти окно SM
find_sm_window()
# 3. Кликнуть на RDP-сервер в sidebar
click_in_window(sm_hwnd, sidebar_x, server_y)
time.sleep(0.5)
# 4. Переключиться на таб Launch
click_in_window(sm_hwnd, launch_tab_x, tab_y)
time.sleep(0.5)
# 5. Нажать Connect
click_in_window(sm_hwnd, connect_btn_x, connect_btn_y)
time.sleep(5) # Дождаться embed
# 6. Сделать скриншот для верификации
take_screenshot("test_rdp_connected.png")
# 7. Нажать Disconnect
click_in_window(sm_hwnd, disconnect_btn_x, disconnect_btn_y)
time.sleep(1)
# 8. Верификация
take_screenshot("test_rdp_disconnected.png")
```
### Верификация по скриншотам
Скриншоты сохраняются в корень проекта для ручного визуального сравнения. Можно автоматизировать через PIL:
```python
from PIL import Image
def check_pixel_color(screenshot_path, x, y, expected_rgb, tolerance=30):
"""Проверить цвет пикселя (для индикаторов статуса)."""
img = Image.open(screenshot_path)
actual = img.getpixel((x, y))[:3]
for a, e in zip(actual, expected_rgb):
if abs(a - e) > tolerance:
return False
return True
# Пример: проверить что статус зелёный (Connected)
assert check_pixel_color("test_rdp_connected.png", status_x, status_y, (34, 197, 94))
```
---
## Краткий чеклист
| # | Тест | Критерий прохождения |
|---|------|---------------------|
| 1 | Connect + Embed | Окно mstsc внутри фрейма, статус зелёный |
| 2 | Fullscreen toggle | Detach на весь экран → reattach обратно без потери сессии |
| 3 | Reconnect recovery | После разрыва сети mstsc переподключается и автоматически re-embed |
| 4 | Disconnect | mstsc убит, .rdp удалён, UI вернулся к настройкам |
| 5 | Server switch | Старая сессия отключена, новый сервер готов к подключению |
| 6 | Resize | Окно mstsc масштабируется плавно, без артефактов |
---
## Полезные команды для отладки
```bash
# Посмотреть последние RDP-логи
tail -50 ~/.server-connections/app.log | grep -i "rdp\|mstsc\|embed"
# Найти зависшие mstsc процессы
tasklist | findstr mstsc
# Убить все mstsc
taskkill /f /im mstsc.exe
# Проверить временные .rdp файлы
dir %TEMP%\sm_embed_*.rdp
# Проверить реестр доверенных серверов
reg query "HKCU\Software\Microsoft\Terminal Server Client\Servers"
# Проверить AuthenticationLevelOverride
reg query "HKCU\Software\Microsoft\Terminal Server Client" /v AuthenticationLevelOverride
```