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:
chrome-storm-c442
2026-02-24 12:45:05 -05:00
parent 68e94856f6
commit 68f2d7eae8
14 changed files with 529 additions and 26 deletions

View File

@@ -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: