fix: keep manual USB selection available during keyboard grabs

This commit is contained in:
Codex
2026-10-13 04:29:37 +03:00
parent 48ee598e9f
commit 38914cf67f
2 changed files with 145 additions and 4 deletions
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Disposable headless GUI checks. Never connects to an inherited display."""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from hydra.profiles import Profile
from hydra.runtime import Runtime
from hydra.session import build_labwc_config, build_viewer_argv, _viewer_environment
def wait_for(predicate, description, timeout=8):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
time.sleep(.05)
raise AssertionError(description)
def main():
cli = argparse.ArgumentParser(description=__doc__)
cli.add_argument('--viewer', default=os.environ.get('HYDRA_VIEWER', 'remote-viewer'))
cli.add_argument('--labwc', default=os.environ.get('HYDRA_LABWC', 'labwc'))
cli.add_argument('--keyboard', default='wtype')
args = cli.parse_args()
for name in ('viewer', 'labwc', 'keyboard'):
value = shutil.which(getattr(args, name))
if not value:
cli.error(f'missing {name}: {getattr(args, name)}')
setattr(args, name, value)
root = Path(tempfile.mkdtemp(prefix='hydra-smoke-'))
print(f'Evidence: {root}', flush=True)
xdg = root / 'xdg'
xdg.mkdir(mode=0o700)
config = root / 'labwc'
config.mkdir()
(config / 'rc.xml').write_text(build_labwc_config(['probe'], ['/bin/true']))
(config / 'autostart').write_text('#!/bin/sh\n')
(config / 'environment').touch()
env = dict(os.environ, XDG_RUNTIME_DIR=str(xdg), WLR_BACKENDS='headless',
WLR_RENDERER='pixman', WLR_XWAYLAND='/bin/false', XDG_DATA_DIRS='/nonexistent',
GSETTINGS_BACKEND='memory')
for key in ('DISPLAY', 'WAYLAND_DISPLAY'):
env.pop(key, None)
manager = Runtime(root / 'config', root / 'state', root / 'run')
manager.add(Profile('probe', memory=128, cpus=1, disk_size='8M', graphics='software'))
processes = []
try:
with (root / 'labwc.log').open('w') as cl, (root / 'viewer.log').open('w') as vl:
compositor = subprocess.Popen([args.labwc, '-d', '-C', str(config)], env=env, stdout=cl, stderr=cl)
processes.append(compositor)
wait_for(lambda: list(xdg.glob('wayland-*')), 'private display did not start')
env['WAYLAND_DISPLAY'] = next(p.name for p in xdg.glob('wayland-*') if not p.name.endswith('.lock'))
keyboard = subprocess.Popen([args.keyboard, '-k', 'Escape', '-s', '30000'], env=env)
processes.append(keyboard)
time.sleep(.2)
manager.start('probe')
previous = dict(os.environ)
try:
os.environ.update(env)
viewer_env = _viewer_environment(root / 'viewer-state')
finally:
os.environ.clear()
os.environ.update(previous)
viewer_env['WAYLAND_DEBUG'] = '1'
viewer = subprocess.Popen(build_viewer_argv(manager.paths('probe'), args.viewer),
env=viewer_env, stdout=vl, stderr=vl)
processes.append(viewer)
log = root / 'viewer.log'
wait_for(lambda: 'set_title("HydraOS: probe")' in log.read_text(), 'viewer did not connect')
time.sleep(.5)
title = 'set_title("Select USB devices for redirection")'
assert title not in log.read_text(), 'USB selector opened automatically'
subprocess.run([args.keyboard, '-M', 'ctrl', '-M', 'alt', '-M', 'shift', '-k', 'u',
'-m', 'shift', '-m', 'alt', '-m', 'ctrl'], env=env, check=True)
wait_for(lambda: title in log.read_text(), 'manual USB selector did not open', timeout=3)
viewer.terminate()
viewer.wait(timeout=5)
assert manager.status('probe')['status'] == 'running'
compositor.terminate()
compositor.wait(timeout=5)
assert manager.status('probe')['status'] == 'running'
result = {'manual_usb_dialog': True, 'automatic_usb_dialog': False,
'qemu_survives_viewer_and_compositor': True,
'viewer_version': subprocess.check_output([args.viewer, '--version'], text=True).strip()}
(root / 'result.json').write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(result, indent=2))
finally:
for proc in reversed(processes):
if proc.poll() is None:
proc.terminate()
proc.wait(timeout=5)
if manager.status('probe')['status'] != 'stopped':
manager.stop('probe', force=True)
if __name__ == '__main__':
main()
+39 -4
View File
@@ -8,20 +8,20 @@
gboolean attach;
gboolean shared;
gboolean quitting;
@@ -951,6 +952,13 @@
{
@@ -952,6 +953,13 @@
gchar *subtitle = NULL;
const gchar *title = virt_viewer_app_get_title(app);
+
+ /* Hidden fullscreen windows use the requested title as stable identity. */
+ if (title != NULL && virt_viewer_app_get_hide_toolbar(app) &&
+ virt_viewer_app_get_fullscreen(app)) {
+ g_object_set(window, "subtitle", title, NULL);
+ return;
+ }
+
if (title != NULL) {
VirtViewerDisplay *display = virt_viewer_window_get_display(window);
gchar *d = strstr(title, "%d");
@@ -2134,6 +2142,7 @@
static gboolean opt_verbose = FALSE;
static gboolean opt_debug = FALSE;
@@ -110,3 +110,38 @@
gtk_window_set_title(GTK_WINDOW(self->window), title);
if (self->subtitle) {
gtk_header_bar_set_title(GTK_HEADER_BAR(header), self->subtitle);
@@ -1467,6 +1475,34 @@
display = self->display;
event = (GdkEventKey *)ev;
+ /* Keyboard grabs remove GTK's application accelerator group so normal
+ * shortcuts reach the guest. Keep only the explicitly configured manual
+ * USB selector available before forwarding this event to the display. */
+ if (!self->kiosk) {
+ gchar **accels = gtk_application_get_accels_for_action(
+ GTK_APPLICATION(self->app), "win.usb-device-select");
+ gboolean matched = FALSE;
+ for (guint i = 0; accels[i]; i++) {
+ guint key;
+ GdkModifierType mods;
+ gtk_accelerator_parse(accels[i], &key, &mods);
+ if (key && gdk_keyval_to_lower(event->keyval) == key &&
+ (event->state & gtk_accelerator_get_default_mod_mask()) == mods) {
+ matched = TRUE;
+ break;
+ }
+ }
+ g_strfreev(accels);
+ if (matched) {
+ GAction *action = g_action_map_lookup_action(
+ G_ACTION_MAP(self->window), "usb-device-select");
+ if (action && g_action_get_enabled(action)) {
+ g_action_activate(action, NULL);
+ return TRUE;
+ }
+ }
+ }
+
gtk_widget_grab_focus(GTK_WIDGET(display));
// Look through keymaps - if set for mappings and intercept