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

21 KiB
Raw Blame History

Ручное тестирование 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 масштабирование) необходимо учитывать разницу между логическими и физическими пикселями:

# Получение 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