Refuse window input when Windows cannot focus its target

This commit is contained in:
saphid committed 2026-09-28 23:07:38 +10:00
1 parent da5c954ce4
commit 6104c3f192
3 files changed
+30 -3

No files matched your search

+3 -1
View File
@@ -41,7 +41,9 @@ API choices are **documented**, not device verification:
Windows needs a WGC-capable Windows 10/11 desktop and an available hardware Windows needs a WGC-capable Windows 10/11 desktop and an available hardware
Media Foundation H.264 encoder. Elevated windows and the secure desktop Media Foundation H.264 encoder. Elevated windows and the secure desktop
cannot be driven by an ordinary Frame Control process. Minimized/closed cannot be driven by an ordinary Frame Control process. If Windows refuses
to focus a selected window, input is refused rather than sent to the app
covering it; bring the selected window forward, then Stop and Show again. Minimized/closed
windows may stop producing frames. Protected content is not supported. windows may stop producing frames. Protected content is not supported.
On Linux, press **Choose a window or screen…** and approve the desktop's On Linux, press **Choose a window or screen…** and approve the desktop's
+10
View File
@@ -93,6 +93,16 @@ class AdapterTests(unittest.TestCase):
self.assertIn(mock.call(mouse=(0, 0, 0, 0x10)), host.send.call_args_list) self.assertIn(mock.call(mouse=(0, 0, 0, 0x10)), host.send.call_args_list)
self.assertIn(mock.call(key=(162, 0, 2)), host.send.call_args_list) self.assertIn(mock.call(key=(162, 0, 2)), host.send.call_args_list)
def test_refused_windows_focus_never_clicks_the_covering_app(self):
host = mock.Mock()
host.rect.return_value = (0, 0, 800, 600)
host.user.GetForegroundWindow.return_value = 99
host.user.SetForegroundWindow.return_value = False
inp = capture.WindowsInput(host, dict(src='window:12', w=800, h=600))
with self.assertRaisesRegex(RuntimeError, 'could not focus'):
inp.handle(dict(t='m', e='down', b=0, x=.5, y=.5))
host.send.assert_not_called()
def test_pc_reuses_launcher_and_namespaces_panel_ids(self): def test_pc_reuses_launcher_and_namespaces_panel_ids(self):
view = frame_pcview.PCView(['ssh'], mock.Mock(return_value='panel created'), 'frame') view = frame_pcview.PCView(['ssh'], mock.Mock(return_value='panel created'), 'frame')
view.call = mock.Mock(side_effect=lambda path, **kw: {'screen': True, 'ticket': 'one-use', 'streams': []}) view.call = mock.Mock(side_effect=lambda path, **kw: {'screen': True, 'ticket': 'one-use', 'streams': []})
+17 -2
View File
@@ -217,6 +217,8 @@ class Windows:
self.user.GetWindowTextW.argtypes = [W.HWND, W.LPWSTR, C.c_int] self.user.GetWindowTextW.argtypes = [W.HWND, W.LPWSTR, C.c_int]
self.user.GetWindowRect.argtypes = [W.HWND, C.POINTER(W.RECT)] self.user.GetWindowRect.argtypes = [W.HWND, C.POINTER(W.RECT)]
self.user.SetForegroundWindow.argtypes = [W.HWND] self.user.SetForegroundWindow.argtypes = [W.HWND]
self.user.GetForegroundWindow.argtypes = []
self.user.GetForegroundWindow.restype = W.HWND
self.dwm.DwmGetWindowAttribute.argtypes = [W.HWND, W.DWORD, C.c_void_p, W.DWORD] self.dwm.DwmGetWindowAttribute.argtypes = [W.HWND, W.DWORD, C.c_void_p, W.DWORD]
self.callback = C.WINFUNCTYPE(W.BOOL, W.HWND, W.LPARAM) self.callback = C.WINFUNCTYPE(W.BOOL, W.HWND, W.LPARAM)
self.monitor_callback = C.WINFUNCTYPE(W.BOOL, W.HMONITOR, W.HDC, C.POINTER(W.RECT), W.LPARAM) self.monitor_callback = C.WINFUNCTYPE(W.BOOL, W.HMONITOR, W.HDC, C.POINTER(W.RECT), W.LPARAM)
@@ -305,6 +307,16 @@ class WindowsInput:
if error: if error:
raise error raise error
def focus(self):
if not self.source['src'].startswith('window:'):
return
hwnd = int(self.source['src'].split(':')[1])
user = self.host.user
if user.GetForegroundWindow() != hwnd and not user.SetForegroundWindow(hwnd):
raise RuntimeError('Windows could not focus this window. Bring it to the front on your PC, then press Stop and Show again.')
if user.GetForegroundWindow() != hwnd:
raise RuntimeError('The selected window is not in front. Reopen the view after bringing it to the front on your PC.')
def handle(self, m): def handle(self, m):
t, u = m['t'], self.host.user t, u = m['t'], self.host.user
if t == 'release': if t == 'release':
@@ -314,8 +326,8 @@ class WindowsInput:
if source['src'].startswith('window:'): if source['src'].startswith('window:'):
hwnd = int(source['src'].split(':')[1]) hwnd = int(source['src'].split(':')[1])
x, y, w, h = self.host.rect(hwnd) x, y, w, h = self.host.rect(hwnd)
if m.get('e') == 'down': if m.get('e') == 'down' or t == 'wheel':
u.SetForegroundWindow(hwnd) self.focus()
else: else:
x, y, w, h = (source[k] for k in ('x', 'y', 'w', 'h')) x, y, w, h = (source[k] for k in ('x', 'y', 'w', 'h'))
x += max(0, min(1, float(m.get('x', 0)))) * (w - 1) x += max(0, min(1, float(m.get('x', 0)))) * (w - 1)
@@ -335,6 +347,8 @@ class WindowsInput:
self.host.send(mouse=(0, 0, delta & 0xffffffff, flag)) self.host.send(mouse=(0, 0, delta & 0xffffffff, flag))
elif t == 'k': elif t == 'k':
code, down = str(m.get('code', '')), m.get('e') == 'down' code, down = str(m.get('code', '')), m.get('e') == 'down'
if down:
self.focus()
special = {'Enter': 13, 'Escape': 27, 'Tab': 9, 'Backspace': 8, 'Space': 32, special = {'Enter': 13, 'Escape': 27, 'Tab': 9, 'Backspace': 8, 'Space': 32,
'ArrowLeft': 37, 'ArrowUp': 38, 'ArrowRight': 39, 'ArrowDown': 40, 'ArrowLeft': 37, 'ArrowUp': 38, 'ArrowRight': 39, 'ArrowDown': 40,
'Delete': 46, 'Home': 36, 'End': 35, 'PageUp': 33, 'PageDown': 34, 'Delete': 46, 'Home': 36, 'End': 35, 'PageUp': 33, 'PageDown': 34,
@@ -350,6 +364,7 @@ class WindowsInput:
elif down and len(str(m.get('key', ''))) == 1: elif down and len(str(m.get('key', ''))) == 1:
self.handle({'t': 'text', 's': m['key']}) self.handle({'t': 'text', 's': m['key']})
elif t == 'text': elif t == 'text':
self.focus()
raw = str(m.get('s', ''))[:4096].encode('utf-16-le') raw = str(m.get('s', ''))[:4096].encode('utf-16-le')
for i in range(0, len(raw), 2): for i in range(0, len(raw), 2):
unit = int.from_bytes(raw[i:i+2], 'little') unit = int.from_bytes(raw[i:i+2], 'little')