- 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>
454 lines
21 KiB
Markdown
454 lines
21 KiB
Markdown
# Ручное тестирование 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
|
||
```
|