# Ручное тестирование 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. Восстановить размер окна ### Ожидаемый результат - При изменении размера фрейма срабатывает `` 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 ```