154 lines
9.7 KiB
Python
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()
|