Files
cthulhu/tests/wine_review_x11.py

154 lines
9.7 KiB
Python

"""Private-display physical-key driver for the Go Wine review acceptance test."""
import ctypes
import json
import os
import re
from pathlib import Path
import subprocess
import sys
import tempfile
import time
sys.dont_write_bytecode = True
from native_x11 import set_active_window, wait_for
def chord(symbols):
x11 = ctypes.CDLL('libX11.so.6')
xtst = ctypes.CDLL('libXtst.so.6')
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
x11.XOpenDisplay.restype = ctypes.c_void_p
x11.XKeysymToKeycode.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
x11.XKeysymToKeycode.restype = ctypes.c_ubyte
x11.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int]
x11.XCloseDisplay.argtypes = [ctypes.c_void_p]
xtst.XTestFakeKeyEvent.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_int, ctypes.c_ulong]
display = x11.XOpenDisplay(None)
assert display
codes = [x11.XKeysymToKeycode(display, value) for value in symbols]
assert all(codes)
try:
for code in codes:
assert xtst.XTestFakeKeyEvent(display, code, 1, 0)
for code in reversed(codes):
assert xtst.XTestFakeKeyEvent(display, code, 0, 0)
x11.XSync(display, 0)
finally:
x11.XCloseDisplay(display)
def main():
build = Path(os.environ['CTHULHU_WINE_REVIEW_BUILD'])
fixture = build / ('review-fixture32.exe' if os.environ.get('CTHULHU_WINE_REVIEW_32') == '1' else 'review-fixture64.exe')
processes = []
window_manager = None
with tempfile.TemporaryDirectory(prefix='wine-review-keys-') as directory:
if os.environ.get('CTHULHU_WINE_REVIEW_WM') == 'i3':
config=Path(directory)/'i3.conf'
config.write_text('font pango:monospace 10\ndefault_border pixel 2\nfocus_follows_mouse no\n')
window_manager=subprocess.Popen(['i3','-a','-c',str(config)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
wait_for(lambda: '0x' in subprocess.run(['xprop','-root','_NET_SUPPORTING_WM_CHECK'],capture_output=True,text=True).stdout,'private i3 did not start',5)
env = dict(os.environ, WINEPREFIX=str(Path(directory)/'prefix'), WINEDEBUG='-all', WINEDLLOVERRIDES='mscoree,mshtml=', CTHULHU_WINE_ACCESS_MANAGED='1')
secondary_env, secondary_window = None, None
def configure(mode):
subprocess.run(['wine', str(fixture), '--configure', str(mode)],env=env,check=True,timeout=5,stdout=subprocess.DEVNULL)
def activate():
found = subprocess.run(['xdotool','search','--onlyvisible','--name','^Wine review fixture$'],capture_output=True,text=True)
if not found.stdout.strip():return False
candidates = [int(value) for value in found.stdout.splitlines() if int(value) != secondary_window]
if not candidates:return False
window = candidates[0]
if window_manager is not None:
subprocess.run(['xdotool','windowactivate','--sync',str(window)],check=True)
subprocess.run(['xdotool','windowfocus',str(window)],check=True)
set_active_window(window)
return window
linux = None
def activate_linux():
nonlocal linux
if linux is None:
source = ('import gi; gi.require_version("Gtk", "3.0"); '
'from gi.repository import Gtk, GLib; '
'window=Gtk.Window(title="Wine review Linux fixture"); '
'window.set_default_size(400,250); view=Gtk.TextView(); '
'view.get_buffer().set_text("Linux review ready\\nnext Linux"); '
'view.get_buffer().place_cursor(view.get_buffer().get_start_iter()); '
'window.add(view); window.show_all(); view.grab_focus(); Gtk.main()')
linux = subprocess.Popen([sys.executable, '-c', source],stdout=subprocess.DEVNULL)
processes.append(linux)
def focus():
result = subprocess.run(['xdotool','search','--onlyvisible','--name','^Wine review Linux fixture$'],capture_output=True,text=True)
if not result.stdout.strip():return False
window = int(result.stdout.splitlines()[0])
subprocess.run(['xdotool','windowfocus',str(window)],check=True)
set_active_window(window)
return True
wait_for(focus,'Linux fixture missing',5)
try:
subprocess.run(['wineboot','-u'],env=env,check=True,timeout=45,stdout=subprocess.DEVNULL)
processes.append(subprocess.Popen([str(build/'wine-access.exe'),'--no-controller'],env=env,stdout=subprocess.DEVNULL))
processes.append(subprocess.Popen(['wine',str(fixture)],env=env,stdout=subprocess.DEVNULL))
wait_for(activate,'native Wine window missing',10)
window = activate()
diagnostics = subprocess.run(['xprop', '-id', str(window), 'WM_CLASS', '_NET_WM_PID'],capture_output=True,text=True)
print(diagnostics.stdout,file=sys.stderr,flush=True)
match = re.search(r'_NET_WM_PID\(CARDINAL\) = (\d+)', diagnostics.stdout)
if match:print('Wine executable: '+os.readlink('/proc/'+match.group(1)+'/exe'),file=sys.stderr,flush=True)
configure(12)
print(json.dumps({'ready':True}),flush=True)
for line in sys.stdin:
command = json.loads(line)
action = command['action']
if action == 'keys':chord(command['symbols'])
elif action == 'configure':configure(command['mode']);activate()
elif action == 'check':
subprocess.run(['wine',str(fixture),command.get('check','--check-strict')],env=env,check=True,timeout=5,stdout=subprocess.DEVNULL)
elif action == 'restart-helper':
processes[0].terminate();processes[0].wait(timeout=3)
processes[0]=subprocess.Popen([str(build/'wine-access.exe'),'--no-controller'],env=env,stdout=subprocess.DEVNULL)
elif action == 'linux':activate_linux()
elif action == 'wine':activate()
elif action == 'second-prefix':
known = set(subprocess.run(['xdotool','search','--onlyvisible','--name','^Wine review fixture$'],capture_output=True,text=True).stdout.splitlines())
secondary_env = dict(env, WINEPREFIX=str(Path(directory)/'second-prefix'))
subprocess.run(['wineboot','-u'],env=secondary_env,check=True,timeout=45,stdout=subprocess.DEVNULL)
processes.append(subprocess.Popen([str(build/'wine-access.exe'),'--no-controller'],env=secondary_env,stdout=subprocess.DEVNULL))
processes.append(subprocess.Popen(['wine',str(fixture)],env=secondary_env,stdout=subprocess.DEVNULL))
def focus_second():
found = subprocess.run(['xdotool','search','--onlyvisible','--name','^Wine review fixture$'],capture_output=True,text=True)
fresh = [value for value in found.stdout.splitlines() if value not in known]
return int(fresh[0]) if fresh else None
secondary_window = wait_for(focus_second,'second-prefix window missing',10)
subprocess.run(['xdotool','windowfocus',str(secondary_window)],check=True)
set_active_window(secondary_window)
elif action == 'virtual-desktop':
# Configure the same default desktop used by winecfg. A
# helper on another named Windows desktop cannot inspect
# this desktop's foreground, even within the same prefix.
subprocess.run(['wine','reg','add',r'HKCU\Software\Wine\Explorer','/v','Desktop','/d','Default','/f'],env=env,check=True,stdout=subprocess.DEVNULL,timeout=5)
subprocess.run(['wine','reg','add',r'HKCU\Software\Wine\Explorer\Desktops','/v','Default','/d','900x700','/f'],env=env,check=True,stdout=subprocess.DEVNULL,timeout=5)
subprocess.run(['wineserver','-k'],env=env,check=True,timeout=5)
processes[0].wait(timeout=3);processes[1].wait(timeout=3)
processes[0]=subprocess.Popen([str(build/'wine-access.exe'),'--no-controller'],env=env,stdout=subprocess.DEVNULL)
processes[1]=subprocess.Popen(['wine',str(fixture)],env=env,stdout=subprocess.DEVNULL)
window = wait_for(activate,'virtual-desktop app missing',10)
result = subprocess.run(['xdotool','search','--onlyvisible','--name','Wine Desktop'],capture_output=True,text=True,check=True)
assert result.stdout.strip(), 'Wine desktop container missing'
subprocess.run(['xdotool','windowfocus',str(window)],check=True)
set_active_window(int(result.stdout.splitlines()[0]))
diagnostics = subprocess.run(['xwininfo','-id',str(window),'-tree'],capture_output=True,text=True)
print(diagnostics.stdout,file=sys.stderr,flush=True)
elif action == 'quit':break
else:raise ValueError(action)
print(json.dumps({'ready':True}),flush=True)
finally:
for process in reversed(processes):
if process.poll() is None:
process.terminate()
try:process.wait(timeout=3)
except subprocess.TimeoutExpired:process.kill();process.wait(timeout=3)
subprocess.run(['wineserver','-k'],env=env,timeout=5)
if secondary_env is not None:subprocess.run(['wineserver','-k'],env=secondary_env,timeout=5)
if window_manager is not None:
window_manager.terminate();window_manager.wait(timeout=3)
if __name__ == '__main__':main()