fix: keep manual USB selection available during keyboard grabs
This commit is contained in:
+106
@@ -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()
|
||||||
@@ -8,20 +8,20 @@
|
|||||||
gboolean attach;
|
gboolean attach;
|
||||||
gboolean shared;
|
gboolean shared;
|
||||||
gboolean quitting;
|
gboolean quitting;
|
||||||
@@ -951,6 +952,13 @@
|
@@ -952,6 +953,13 @@
|
||||||
{
|
|
||||||
gchar *subtitle = NULL;
|
gchar *subtitle = NULL;
|
||||||
const gchar *title = virt_viewer_app_get_title(app);
|
const gchar *title = virt_viewer_app_get_title(app);
|
||||||
+
|
|
||||||
+ /* Hidden fullscreen windows use the requested title as stable identity. */
|
+ /* Hidden fullscreen windows use the requested title as stable identity. */
|
||||||
+ if (title != NULL && virt_viewer_app_get_hide_toolbar(app) &&
|
+ if (title != NULL && virt_viewer_app_get_hide_toolbar(app) &&
|
||||||
+ virt_viewer_app_get_fullscreen(app)) {
|
+ virt_viewer_app_get_fullscreen(app)) {
|
||||||
+ g_object_set(window, "subtitle", title, NULL);
|
+ g_object_set(window, "subtitle", title, NULL);
|
||||||
+ return;
|
+ return;
|
||||||
+ }
|
+ }
|
||||||
|
+
|
||||||
if (title != NULL) {
|
if (title != NULL) {
|
||||||
VirtViewerDisplay *display = virt_viewer_window_get_display(window);
|
VirtViewerDisplay *display = virt_viewer_window_get_display(window);
|
||||||
|
gchar *d = strstr(title, "%d");
|
||||||
@@ -2134,6 +2142,7 @@
|
@@ -2134,6 +2142,7 @@
|
||||||
static gboolean opt_verbose = FALSE;
|
static gboolean opt_verbose = FALSE;
|
||||||
static gboolean opt_debug = FALSE;
|
static gboolean opt_debug = FALSE;
|
||||||
@@ -110,3 +110,38 @@
|
|||||||
gtk_window_set_title(GTK_WINDOW(self->window), title);
|
gtk_window_set_title(GTK_WINDOW(self->window), title);
|
||||||
if (self->subtitle) {
|
if (self->subtitle) {
|
||||||
gtk_header_bar_set_title(GTK_HEADER_BAR(header), 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
|
||||||
|
|||||||
Reference in New Issue
Block a user