Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions packages/flow/openadapt_flow/backends/win_agent/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,24 @@ def rect(self, hwnd: int) -> Optional[tuple[int, int, int, int]]:
return None
return (int(r.left), int(r.top), int(r.right), int(r.bottom))

def outer_rect(self, hwnd: int) -> Optional[tuple[int, int, int, int]]:
"""Everything the window may paint: GetWindowRect joined with the
visible frame (a self-drawn frame can paint into the border DWM
leaves out)."""
r = self._wintypes.RECT()
frame = self.rect(hwnd)
if not self._dll.GetWindowRect(hwnd, self._ctypes.byref(r)):
return frame
outer = (int(r.left), int(r.top), int(r.right), int(r.bottom))
if frame is None:
return outer
return (
min(outer[0], frame[0]),
min(outer[1], frame[1]),
max(outer[2], frame[2]),
max(outer[3], frame[3]),
)

def per_monitor_dpi(self) -> bool:
"""Put this thread in per-monitor-v2 DPI awareness, so window rects
and points are physical pixels like the screenshot's."""
Expand Down Expand Up @@ -1702,8 +1720,10 @@ def _window_operation(
to the captured window and answers with a window only when all of this
holds: no visible window of another process, and none of the captured
process that its owner chain does not tie to the captured window, meets
the region anywhere above the captured window; and an owned window
covers the region's center (its visible frame, not the resize border).
the region anywhere above the captured window; and an owned window meets
the region (its visible frame, not the resize border). The topmost such
window is the answer. Whether the covered part hides anything that
matters is for the caller, which still sees the uncovered part.
A captured window that is gone or changed process, a cloak state that
cannot be read for one of the app's windows, and anything else answer
``None``.
Expand Down Expand Up @@ -1755,7 +1775,6 @@ def _window_operation(
if not user32.is_window(hwnd) or user32.pid_of(hwnd) != pid:
return {"status": "ok", "window": None}
box = (region[0], region[1], region[2], region[3])
center = (region[0] + region[2] // 2, region[1] + region[3] // 2)
owned: Optional[dict[str, Any]] = None
reached = False
for candidate in user32.top_level_windows():
Expand All @@ -1770,7 +1789,9 @@ def _window_operation(
return {"status": "ok", "window": None} # cannot tell if it shows
if cloaked:
continue
rect = user32.rect(candidate)
# Another process's window is measured by everything it may paint;
# the app's own by its visible frame.
rect = user32.rect(candidate) if same_process else user32.outer_rect(candidate)
if rect is None or rect[2] <= rect[0] or rect[3] <= rect[1]:
continue
if not _intersects(rect, box):
Expand All @@ -1785,10 +1806,7 @@ def _window_operation(
link = user32.owner(link)
if link != hwnd:
return {"status": "ok", "window": None} # not tied to the capture
covers_center = (
rect[0] <= center[0] < rect[2] and rect[1] <= center[1] < rect[3]
)
if owned is None and covers_center:
if owned is None:
owned = {
"title": user32.title(candidate)[:512] or None,
"hwnd": candidate,
Expand Down
5 changes: 3 additions & 2 deletions packages/flow/openadapt_flow/backends/windows_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,8 +861,9 @@ def owned_window_over(
:meth:`record_verified_window`). Returns ``{"title", "hwnd",
"owner_hwnd", "pid"}`` only when the first visible window above the
recorded one, of the same process and with an owner chain reaching it
(an owned dialog, a message box), covers the region's center, and no
window of another process, or unowned, meets the region above it. Returns None with no record for that step, when the
(an owned dialog, a message box), meets the region, and no window of
another process, or unowned, meets the region above it. The topmost
owned window that meets it is returned. Returns None with no record for that step, when the
recorded window is gone or now another process's, for any other
process's window or an unowned one, and on any failure. Never raises.
"""
Expand Down
43 changes: 37 additions & 6 deletions packages/flow/tests/test_win_agent_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,7 @@ def __init__(self) -> None:
self.dpi_ok = True
self.foreground_window = self.PINNED
self.roots: dict[int, int] = {} # child hwnd -> top-level
self.paint_border: dict[int, int] = {} # self-drawn frame, in px

def add(
self,
Expand Down Expand Up @@ -1279,6 +1280,11 @@ def owner(self, hwnd):
def rect(self, hwnd):
return self.windows[hwnd][2]

def outer_rect(self, hwnd):
x0, y0, x1, y1 = self.windows[hwnd][2]
grow = self.paint_border.get(hwnd, 0)
return (x0 - grow, y0 - grow, x1 + grow, y1 + grow)

def is_visible(self, hwnd):
return self.windows[hwnd][3]

Expand Down Expand Up @@ -1338,10 +1344,37 @@ def test_a_topmost_owned_popup_does_not_hide_a_foreign_window_over_the_target(
assert _owned_over(monkeypatch, desktop) is None


def test_an_owned_window_that_only_clips_the_region_is_none(monkeypatch) -> None:
# A shadow or glow window of the app's own grazing one corner.
def test_an_owned_dialog_over_part_of_the_region_is_reported(monkeypatch) -> None:
# Live lab finding: the "Session notice" dialog covers the right part of
# the identity band, not its center; the caller still sees the rest.
desktop = _FakeDesktop()
desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(295, 265, 400, 300))
assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x200


def test_a_foreign_self_drawn_frame_reaching_the_band_is_none(monkeypatch) -> None:
# Review should-fix: an Electron-style window paints into the ~7 px border
# DWM leaves out of its visible frame.
desktop = _FakeDesktop()
desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(280, 240, 400, 300))
desktop.add(0x900, pid=99, rect=(200, 240, 245, 300)) # frame ends 5 px short
desktop.paint_border[0x900] = 7
assert _owned_over(monkeypatch, desktop) is None


def test_a_foreign_window_beside_an_owned_dialog_over_the_band_is_none(
monkeypatch,
) -> None:
# Review attack (a): the app's dialog covers one end of the band and
# another process's window the other end.
desktop = _FakeDesktop()
desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(280, 240, 400, 300))
desktop.add(0x900, pid=99, rect=(200, 240, 262, 300))
assert _owned_over(monkeypatch, desktop) is None
# Same with the foreign window below the dialog in z-order.
desktop = _FakeDesktop()
desktop.add(0x900, pid=99, rect=(200, 240, 262, 300))
desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(280, 240, 400, 300))
assert _owned_over(monkeypatch, desktop) is None


Expand Down Expand Up @@ -1389,13 +1422,11 @@ def test_a_dialog_owned_through_a_chain_counts(monkeypatch) -> None:
assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x300


def test_an_owned_window_over_the_center_wins_over_one_only_clipping(
monkeypatch,
) -> None:
def test_the_topmost_owned_window_over_the_region_is_reported(monkeypatch) -> None:
desktop = _FakeDesktop()
desktop.add(0x200, owner=_FakeDesktop.PINNED, title="Session notice")
desktop.add(0x300, owner=_FakeDesktop.PINNED, rect=(295, 265, 400, 300))
assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x200
assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x300


@pytest.mark.parametrize(
Expand Down
Loading