v1.8.35: fix embedded RDP auto-recovery on reconnect + release cleanup
- try_reembed() now handles same-HWND reparent scenario (mstsc reconnect resets parent) - is_embedded() checks GetParent(hwnd) == parent_hwnd every 500ms - _monitor_tick() two-stage: is_alive() for process death, is_embedded() for window loss - build.py auto-cleans old releases (keep first + last 5) - Cleaned old releases from git Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -507,6 +507,8 @@ class EmbeddedRDP:
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
self._parent_hwnd = parent_hwnd
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
hwnd = self._mstsc_hwnd
|
||||
@@ -684,6 +686,104 @@ class EmbeddedRDP:
|
||||
|
||||
log.info("EmbeddedRDP disconnected")
|
||||
|
||||
def try_reembed(self, parent_hwnd: int, width: int, height: int) -> bool:
|
||||
"""Try to re-embed the mstsc window after reconnect.
|
||||
|
||||
Handles two scenarios:
|
||||
1. Same HWND got reparented to desktop (mstsc reconnect resets parent)
|
||||
2. New HWND created (mstsc spawned a new window)
|
||||
|
||||
Returns True if a window was found and embedded.
|
||||
"""
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
# Scenario 1: current HWND is still valid but lost its parent
|
||||
if self._mstsc_hwnd and user32.IsWindow(self._mstsc_hwnd):
|
||||
actual_parent = user32.GetParent(self._mstsc_hwnd)
|
||||
if actual_parent != parent_hwnd:
|
||||
log.info(f"Re-embed: same HWND={self._mstsc_hwnd} lost parent "
|
||||
f"(parent={actual_parent}, expected={parent_hwnd})")
|
||||
self._connected = False
|
||||
self._embed(parent_hwnd, width, height)
|
||||
return self._connected
|
||||
|
||||
# Scenario 2: old HWND invalid, search for new mstsc window
|
||||
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
|
||||
|
||||
hostname = self.server["ip"]
|
||||
mstsc_pids = set(_find_mstsc_pids(self._process.pid)) if self._process else set()
|
||||
|
||||
found_windows = []
|
||||
|
||||
def enum_callback(hwnd, _lparam):
|
||||
if not user32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
class_buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, class_buf, 256)
|
||||
class_name = class_buf.value
|
||||
|
||||
title_buf = ctypes.create_unicode_buffer(512)
|
||||
user32.GetWindowTextW(hwnd, title_buf, 512)
|
||||
title = title_buf.value
|
||||
|
||||
pid = ctypes.wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||
win_pid = pid.value
|
||||
|
||||
is_mstsc_class = class_name in ("TscShellContainerClass", "RAIL_WINDOW", "IHWindowClass")
|
||||
is_dialog = class_name == "#32770"
|
||||
pid_match = win_pid in mstsc_pids
|
||||
title_match = hostname in title
|
||||
|
||||
if (pid_match or title_match) and (is_mstsc_class or is_dialog):
|
||||
if hwnd != self._mstsc_hwnd:
|
||||
found_windows.append((hwnd, class_name, title, win_pid))
|
||||
return True
|
||||
|
||||
user32.EnumWindows(WNDENUMPROC(enum_callback), 0)
|
||||
|
||||
if not found_windows:
|
||||
return False
|
||||
|
||||
# Prefer TscShellContainerClass
|
||||
target = None
|
||||
for hwnd, cls, title, pid in found_windows:
|
||||
if cls == "TscShellContainerClass":
|
||||
target = hwnd
|
||||
break
|
||||
if not target:
|
||||
target = found_windows[0][0]
|
||||
|
||||
log.info(f"Re-embed: found new mstsc window HWND={target}")
|
||||
self._mstsc_hwnd = target
|
||||
self._connected = False
|
||||
self._embed(parent_hwnd, width, height)
|
||||
return self._connected
|
||||
|
||||
def is_embedded(self) -> bool:
|
||||
"""Check if the mstsc HWND is still a child of our parent frame.
|
||||
|
||||
Returns False if:
|
||||
- HWND is invalid (window destroyed)
|
||||
- HWND got reparented away from our frame (reconnect scenario)
|
||||
- No HWND tracked
|
||||
"""
|
||||
if not self._mstsc_hwnd or not self._parent_hwnd:
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
user32 = ctypes.windll.user32
|
||||
if not user32.IsWindow(self._mstsc_hwnd):
|
||||
return False
|
||||
# Check that it's still parented to our frame
|
||||
actual_parent = user32.GetParent(self._mstsc_hwnd)
|
||||
return actual_parent == self._parent_hwnd
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if mstsc process is still running (parent or children)."""
|
||||
if not self._process:
|
||||
|
||||
Reference in New Issue
Block a user