- 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>
21 KiB
Ручное тестирование 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 embeddinggui/tabs/launch_tab.py— UI таба Launch, управление состояниемcore/logger.py— настройки логгера
Тест 1: Первичное подключение и встраивание
Предусловия
- В ServerManager добавлен сервер типа
rdpс валидными credentials - Сервер доступен по сети (порт 3389 открыт)
- Запущен
python main.pyили exe изreleases/
Шаги
- Выбрать RDP-сервер в боковой панели (sidebar)
- Перейти на вкладку Launch
- Убедиться, что видна панель настроек: Quality, Clipboard, Drives, Printers
- Оставить настройки по умолчанию (Auto, Clipboard ON, остальное OFF)
- Нажать кнопку 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" (зелёный)
Шаги
- Нажать кнопку Fullscreen в тулбаре
- Убедиться что окно mstsc развернулось на весь экран
- Поработать в RDP-сессии (открыть Explorer, cmd и т.п.)
- Переключиться обратно в ServerManager (Alt+Tab)
- Нажать кнопку 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-сессия активна и встроена
- Доступ к сетевым настройкам (для имитации разрыва)
Шаги
- При активной RDP-сессии отключить сеть (Wi-Fi off, или вытащить кабель)
- Подождать 5-15 секунд — mstsc покажет "Reconnecting..."
- Включить сеть обратно
- Подождать до 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
- Тот же HWND, другой parent — mstsc reconnect сбросил parent на desktop →
_embed()повторно - Новый HWND — mstsc создал новое окно →
EnumWindowsнаходит по PID или hostname в title
Частые проблемы
| Симптом | Причина | Решение |
|---|---|---|
| mstsc закрылся совсем | Сервер разорвал сессию | is_alive() вернёт False, _on_rdp_exited() покажет панель настроек |
| Повторное встраивание не сработало | Новое окно ещё не появилось | Мониторинг продолжит попытки каждые 500ms пока процесс жив |
| Зависание мониторинга | after() callback не вызвался |
Проверить что GUI main loop не заблокирован |
Тест 4: Disconnect
Предусловия
- RDP-сессия активна
Шаги
- Нажать красную кнопку Disconnect в тулбаре
- Дождаться возврата на панель настроек
Ожидаемый результат
- Мониторинг останавливается (
_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 (любого типа)
Шаги
- При активной RDP-сессии кликнуть на другой сервер в sidebar
- Наблюдать за поведением
Ожидаемый результат
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-сессия активна и встроена
Шаги
- Изменить размер окна ServerManager (потянуть за край)
- Быстро менять размер несколько раз подряд
- Максимизировать окно ServerManager
- Восстановить размер окна
Ожидаемый результат
- При изменении размера фрейма срабатывает
<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 масштабирование) необходимо учитывать разницу между логическими и физическими пикселями:
# Получение 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
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)
Клик по элементам
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)
Автотесты должны гарантировать что после завершения (в т.ч. аварийного) не останется зависших процессов:
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.
Структура автотеста
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:
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 масштабируется плавно, без артефактов |
Полезные команды для отладки
# Посмотреть последние 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