Updates for when prism doesn't behave.

This commit is contained in:
Storm Dragon committed 2026-10-13 17:42:56 -04:00
1 parent 0d9cc2651d
commit 6e92ad13d1
12 files changed
+415 -5

No files matched your search

+4 -2
View File
@@ -19,7 +19,8 @@ add_custom_command(OUTPUT "${menu_hook}"
-static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c"
"${CMAKE_CURRENT_SOURCE_DIR}/scintilla_hook.c" -luser32 -lcomctl32 -lgdi32
DEPENDS menu_hook.c menu_bridge.h edit_hook.c edit_hook.h control_class.h scintilla_hook.c scintilla_messages.h
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c" -loleaut32
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h edit_hook.c edit_hook.h control_class.h scintilla_hook.c scintilla_messages.h
VERBATIM)
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
@@ -30,10 +31,11 @@ add_custom_command(OUTPUT "${menu_hook32}" "${menu_host32}"
-static-libgcc -Wl,--kill-at -o "${menu_hook32}"
"${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c" -luser32 -lcomctl32
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c" -loleaut32
COMMAND "${MINGW32_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -mwindows
-static-libgcc -o "${menu_host32}"
"${CMAKE_CURRENT_SOURCE_DIR}/menu_host32.c" -luser32
DEPENDS menu_hook.c menu_bridge.h menu_host32.c edit_hook.c edit_hook.h control_class.h
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h menu_host32.c edit_hook.c edit_hook.h control_class.h
VERBATIM)
add_custom_target(wine-access-menu32 ALL DEPENDS "${menu_hook32}" "${menu_host32}")
+19 -2
View File
@@ -5,11 +5,13 @@ and readers that implement its Wine D-Bus protocol. An Orca endpoint is
available for future Orca-side integration; stock Orca does not implement
the required Wine methods.
It provides two independent capabilities:
It provides these capabilities:
- an MSAA/WinEvent reader for standard Wine controls;
- NVDA Controller 1, 2, and 3 plus Tolk compatibility for applications using
`nvdaControllerClient32.dll`, `nvdaControllerClient64.dll`, or `Tolk.dll`.
`nvdaControllerClient32.dll`, `nvdaControllerClient64.dll`, or `Tolk.dll`;
- a UIA notification bridge for applications whose announcements Wine discards,
including Prism's UIA speech backend.
The compatibility window is hidden and non-activating. It is expected to be
invisible; the important endpoint is the local Wine RPC server registered by
@@ -17,6 +19,21 @@ the helper. The helper forwards speech, braille, cancellation, and standard
control events to the selected reader on the session D-Bus. The default is
`org.stormux.Cthulhu1.Service`.
Wine currently returns success from `UiaRaiseNotificationEvent` without
delivering its text. The native application hooks redirect direct imports of
that function to the reader. This works with both 32-bit and 64-bit applications;
it requires no application files or per-game DLL overrides. Notifications from
background processes are ignored. Important notifications interrupt speech,
and an empty important notification cancels speech. Each notification is
bounded to 4095 UTF-16 code units without splitting surrogate pairs.
The bridge covers direct named imports, rather than calls obtained dynamically
with `GetProcAddress`. It leaves implementations that reject a missing provider
untouched. The hook DLL remains mapped until the application exits so redirected
imports remain valid across reader restarts. Close and reopen applications after
updating Wine Access to load the new hooks. `--no-dialog-reader` disables these
hooks along with standard control reading.
The executable is named `cthulhu-wine-access.exe` for compatibility with the
Cthulhu-Go supervisor.
@@ -1,6 +1,6 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=wine-access-git
pkgver=r24.g0f59809
pkgver=r29.g0d9cc26
pkgrel=1
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
arch=('x86_64')
+2
View File
@@ -2,6 +2,7 @@
#include "edit_hook.h"
#include "menu_bridge.h"
#include "control_class.h"
#include "notification_hook.h"
#include <commctrl.h>
#include <stdlib.h>
@@ -257,6 +258,7 @@ BOOL wine_access_watch_edit(HWND window) {
__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) {
if (code >= 0 && wp == PM_REMOVE) {
wine_access_watch_notifications();
HWND focus = GetFocus();
if (wine_access_watch_edit(focus))
SendMessageW(focus, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
+31
View File
@@ -17,6 +17,7 @@
#include <rpc.h>
#include <windows.h>
#include <shellapi.h>
#include <uiautomationcore.h>
#include <cstdint>
#include <bit>
@@ -34,6 +35,7 @@ ReviewIdentityStore reviewIdentities;
ReviewService reviewService;
std::deque<WineAccessMenuEvent> menuEvents;
std::deque<WineAccessTextEvent> textEvents;
std::deque<WineAccessNotificationEvent> notificationEvents;
bool editReaderActive = false;
std::string lastMenuEvent;
ULONGLONG lastMenuOwner = 0;
@@ -668,6 +670,18 @@ void present_text_event(const WineAccessTextEvent &event) {
LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_COPYDATA && lp != 0) {
const auto *data = reinterpret_cast<const COPYDATASTRUCT *>(lp);
if (data->dwData == WINE_ACCESS_NOTIFICATION_TAG &&
data->cbData == sizeof(WineAccessNotificationEvent) &&
data->lpData != nullptr && notificationEvents.size() < 32) {
auto event = *static_cast<const WineAccessNotificationEvent *>(data->lpData);
event.text[std::size(event.text) - 1] = L'\0';
notificationEvents.push_back(event);
if (!PostMessageW(window, WINE_ACCESS_MENU_MESSAGE + 2, 0, 0)) {
notificationEvents.pop_back();
return FALSE;
}
return TRUE;
}
if (data->dwData == WINE_ACCESS_TEXT_TAG && data->cbData == sizeof(WineAccessTextEvent) &&
data->lpData != nullptr && textEvents.size() < 32) {
auto event = *static_cast<const WineAccessTextEvent *>(data->lpData);
@@ -710,6 +724,23 @@ LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM l
}
return 0;
}
if (message == WINE_ACCESS_MENU_MESSAGE + 2) {
if (!notificationEvents.empty()) {
const auto event = notificationEvents.front();
notificationEvents.pop_front();
DWORD foregroundProcess = 0;
GetWindowThreadProcessId(GetForegroundWindow(), &foregroundProcess);
if (event.process == foregroundProcess) {
const bool interrupt = event.processing == NotificationProcessing_ImportantAll ||
event.processing == NotificationProcessing_ImportantMostRecent;
if (event.text[0])
call_boolean("SpeakText", g_variant_new("(sb)", to_utf8(event.text).c_str(), interrupt));
else if (interrupt)
call_boolean("CancelSpeech", g_variant_new("()"));
}
}
return 0;
}
return DefWindowProcW(window, message, wp, lp);
}
+7
View File
@@ -20,6 +20,13 @@
#define WINE_ACCESS_TEXT_INSERTED 8
#define WINE_ACCESS_TEXT_SCINTILLA 1
#define WINE_ACCESS_TEXT_READONLY 2
#define WINE_ACCESS_NOTIFICATION_TAG 0x57414e31
typedef struct WineAccessNotificationEvent {
DWORD process;
UINT processing;
WCHAR text[4096];
} WineAccessNotificationEvent;
typedef struct WineAccessTextEvent {
ULONGLONG owner;
+2
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "menu_bridge.h"
#include "edit_hook.h"
#include "notification_hook.h"
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
UINT used = 0;
@@ -19,6 +20,7 @@ static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
__declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
LPARAM lp) {
if (code >= 0) {
wine_access_watch_notifications();
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
+108
View File
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#define COBJMACROS
#include "notification_hook.h"
#include "menu_bridge.h"
#include <oleauto.h>
#include <tlhelp32.h>
#include <uiautomationcore.h>
#include <uiautomationcoreapi.h>
#include <string.h>
// Wine currently drops UiaRaiseNotificationEvent. Replace direct imports in
// applications already reached by our native hook; leave other UIA APIs alone.
// Keep the DLL mapped for the application's lifetime once an import points here.
static HRESULT WINAPI raise_notification(IRawElementProviderSimple *provider,
enum NotificationKind kind, enum NotificationProcessing processing,
BSTR displayString, BSTR activityId) {
(void)kind; (void)activityId;
if (!provider) return E_INVALIDARG;
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
if (!bridge) return S_OK;
WineAccessNotificationEvent event = {0};
event.process = GetCurrentProcessId();
event.processing = processing;
UINT length = displayString ? SysStringLen(displayString) : 0;
if (length >= ARRAYSIZE(event.text)) length = ARRAYSIZE(event.text) - 1;
if (length && displayString[length - 1] >= 0xd800 &&
displayString[length - 1] <= 0xdbff) --length;
if (length) memcpy(event.text, displayString, length * sizeof(WCHAR));
COPYDATASTRUCT data = {WINE_ACCESS_NOTIFICATION_TAG, sizeof(event), &event};
DWORD_PTR result = 0;
// The receiver only copies and queues. No reader calls on an app's thread.
SendMessageTimeoutW(bridge, WM_COPYDATA, 0, (LPARAM)&data,
SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
return S_OK;
}
static void watch_module(BYTE *base) {
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return;
IMAGE_NT_HEADERS *header = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
if (header->Signature != IMAGE_NT_SIGNATURE) return;
IMAGE_DATA_DIRECTORY directory =
header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
if (!directory.VirtualAddress || !directory.Size) return;
IMAGE_IMPORT_DESCRIPTOR *imports =
(IMAGE_IMPORT_DESCRIPTOR *)(base + directory.VirtualAddress);
for (; imports->Name; ++imports) {
if (lstrcmpiA((char *)(base + imports->Name), "uiautomationcore.dll") ||
!imports->OriginalFirstThunk) continue;
IMAGE_THUNK_DATA *names = (IMAGE_THUNK_DATA *)(base + imports->OriginalFirstThunk);
IMAGE_THUNK_DATA *addresses = (IMAGE_THUNK_DATA *)(base + imports->FirstThunk);
for (; names->u1.AddressOfData; ++names, ++addresses) {
if (IMAGE_SNAP_BY_ORDINAL(names->u1.Ordinal)) continue;
IMAGE_IMPORT_BY_NAME *name = (IMAGE_IMPORT_BY_NAME *)(base + names->u1.AddressOfData);
if (strcmp((char *)name->Name, "UiaRaiseNotificationEvent") ||
addresses->u1.Function == (ULONG_PTR)raise_notification) continue;
// The Wine stub even accepts a missing required provider. Leave an
// implementation which rejects it (including native UIA) untouched.
typedef HRESULT (WINAPI *RaiseNotification)(IRawElementProviderSimple *,
enum NotificationKind, enum NotificationProcessing, BSTR, BSTR);
RaiseNotification original = (RaiseNotification)(ULONG_PTR)addresses->u1.Function;
if (original(NULL, NotificationKind_ActionCompleted,
NotificationProcessing_All, NULL, NULL) != S_OK) continue;
HMODULE pinnedModule = NULL;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_PIN, (LPCWSTR)(ULONG_PTR)raise_notification,
&pinnedModule)) continue;
DWORD protection;
if (!VirtualProtect(&addresses->u1.Function, sizeof(addresses->u1.Function),
PAGE_READWRITE, &protection)) continue;
InterlockedExchangePointer((PVOID volatile *)&addresses->u1.Function,
(PVOID)(ULONG_PTR)raise_notification);
DWORD ignored;
VirtualProtect(&addresses->u1.Function, sizeof(addresses->u1.Function),
protection, &ignored);
}
}
}
void wine_access_watch_notifications(void) {
static DWORD lastScan;
static LONG scanning;
DWORD now = GetTickCount();
if (InterlockedCompareExchange(&scanning, 1, 0)) return;
if (lastScan && now - lastScan < 1000) {
InterlockedExchange(&scanning, 0);
return;
}
lastScan = now;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (snapshot != INVALID_HANDLE_VALUE) {
MODULEENTRY32W module = {0};
module.dwSize = sizeof(module);
if (Module32FirstW(snapshot, &module)) {
do {
// Hold a reference while walking imports: another thread may unload it.
HMODULE reference = NULL;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(LPCWSTR)module.modBaseAddr, &reference)) {
watch_module((BYTE *)reference);
FreeLibrary(reference);
}
} while (Module32NextW(snapshot, &module));
}
CloseHandle(snapshot);
}
InterlockedExchange(&scanning, 0);
}
+5
View File
@@ -0,0 +1,5 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <windows.h>
void wine_access_watch_notifications(void);
+23
View File
@@ -1,5 +1,28 @@
# Wine control integration
UIA notification routing uses the actual native application hooks and a private
recording reader, without sending speech to the live reader:
```sh
xvfb-run -a python3 tests/notification_integration.py --build-dir build
xvfb-run -a python3 tests/notification_integration.py --build-dir build --32-bit --reader orca
```
To exercise a real 64-bit Prism DLL through its UIA backend, provide the matching
Prism C headers (`prism.h` and `prism_version.h`) without replacing the DLL:
```sh
xvfb-run -a python3 tests/notification_integration.py --build-dir build \
--prism-dll /path/to/prism.dll --prism-include /path/to/prism/headers
```
These checks cover Unicode text, interruption, empty-message cancellation,
bounded payloads, and ignoring a background application's announcements. They
do not establish audible speech. After installing, restart the reader and game;
in Arcadelux, verify the introduction and repeated Up/Down menu announcements,
including switching away and back. Also check existing NVDA/Tolk speech and
ordinary Wine menus and Edit controls before submitting the change.
## Reader endpoint selection
After building, run these checks sequentially on private displays:
+115
View File
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#define COBJMACROS
#include <windows.h>
#include <oleauto.h>
#include <uiautomationcore.h>
#include <uiautomationcoreapi.h>
#include <stdio.h>
#include <string.h>
#ifdef WINE_ACCESS_PRISM_FIXTURE
#include <shellapi.h>
#define PRISM_STATIC
#include <prism.h>
#define LOAD(name) static __typeof__(&name) p_##name
LOAD(prism_config_init); LOAD(prism_init); LOAD(prism_registry_acquire_best);
LOAD(prism_backend_name); LOAD(prism_backend_speak); LOAD(prism_shutdown);
LOAD(prism_backend_stop);
static PrismContext *context;
static PrismBackend *backend;
#endif
static HWND window;
static int step = -2;
static void notify(const WCHAR *text, enum NotificationProcessing processing) {
#ifdef WINE_ACCESS_PRISM_FIXTURE
char utf8[20000];
WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), NULL, NULL);
BOOL interrupt = processing == NotificationProcessing_ImportantMostRecent ||
processing == NotificationProcessing_ImportantAll;
// Prism rejects empty speak text; its stop command emits an empty important
// UIA notification instead.
PrismError result = utf8[0] ? p_prism_backend_speak(backend, utf8, interrupt) :
interrupt ? p_prism_backend_stop(backend) : PRISM_OK;
if (result != PRISM_OK) ExitProcess(4);
#else
BSTR displayString = SysAllocString(text);
BSTR activity = SysAllocString(L"Fixture");
IRawElementProviderSimple *provider = NULL;
if (FAILED(UiaHostProviderFromHwnd(window, &provider)) || !provider) ExitProcess(2);
HRESULT result = UiaRaiseNotificationEvent(provider, NotificationKind_ActionCompleted,
processing, displayString, activity);
IRawElementProviderSimple_Release(provider);
SysFreeString(displayString);
SysFreeString(activity);
if (FAILED(result)) ExitProcess(3);
#endif
}
static LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_TIMER) {
// Real UIA calls through normal imports, reached by the global native hook.
switch (++step) {
case -1: case 0: break;
case 1: notify(L"Menu caf\u00e9 \U0001f600", NotificationProcessing_All); break;
case 2: notify(L"Second item", NotificationProcessing_ImportantMostRecent); break;
case 3: notify(L"", NotificationProcessing_ImportantMostRecent); break;
case 4: notify(L"", NotificationProcessing_All); break;
case 5: {
WCHAR longText[4100];
for (UINT i = 0; i < ARRAYSIZE(longText) - 1; ++i) longText[i] = L'x';
longText[4094] = 0xd83d; longText[4095] = 0xde00;
longText[ARRAYSIZE(longText) - 1] = 0;
notify(longText, NotificationProcessing_All);
break;
}
case 6: ShowWindow(hwnd, SW_HIDE); break;
case 7: notify(L"Background must stay silent", NotificationProcessing_ImportantAll); break;
default: DestroyWindow(hwnd); break;
}
return 0;
}
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
return DefWindowProcW(hwnd, message, wp, lp);
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
(void)previous; (void)args; (void)show;
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
WNDCLASSW cls = {0};
cls.hInstance = instance; cls.lpfnWndProc = window_proc;
cls.lpszClassName = L"WineAccessNotificationFixture";
RegisterClassW(&cls);
window = CreateWindowW(cls.lpszClassName, L"Notification fixture", WS_OVERLAPPEDWINDOW,
10, 10, 300, 200, NULL, NULL, instance, NULL);
if (!window) return 1;
ShowWindow(window, SW_SHOW); SetForegroundWindow(window);
#ifdef WINE_ACCESS_PRISM_FIXTURE
int argumentCount = 0;
WCHAR **arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
HMODULE module = argumentCount == 2 ? LoadLibraryW(arguments[1]) : NULL;
LocalFree(arguments);
if (!module) return 5;
#undef LOAD
#define LOAD(name) do { FARPROC address = GetProcAddress(module, #name); \
memcpy(&p_##name, &address, sizeof(p_##name)); if (!p_##name) return 6; } while (0)
LOAD(prism_config_init); LOAD(prism_init); LOAD(prism_registry_acquire_best);
LOAD(prism_backend_name); LOAD(prism_backend_speak); LOAD(prism_shutdown);
LOAD(prism_backend_stop);
PrismConfig config = p_prism_config_init();
context = p_prism_init(&config);
if (!context) return 7;
backend = p_prism_registry_acquire_best(context);
if (!backend || strcmp(p_prism_backend_name(backend), "UIA")) return 8;
#endif
SetTimer(window, 1, 500, NULL);
MSG message;
while (GetMessageW(&message, NULL, 0, 0) > 0) {
TranslateMessage(&message); DispatchMessageW(&message);
}
#ifdef WINE_ACCESS_PRISM_FIXTURE
p_prism_shutdown(context);
#endif
CoUninitialize();
return 0;
}
+98
View File
@@ -0,0 +1,98 @@
"""Route real UIA notifications through native hooks to a private reader."""
import argparse
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
from gi.repository import Gio, GLib
parser = argparse.ArgumentParser()
parser.add_argument("--build-dir", default="build")
parser.add_argument("--reader", choices=("cthulhu", "orca"), default="cthulhu")
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
parser.add_argument("--session", action="store_true")
parser.add_argument("--prism-dll", type=Path)
parser.add_argument("--prism-include", type=Path)
args = parser.parse_args()
if bool(args.prism_dll) != bool(args.prism_include) or (args.prism_dll and args.is32Bit):
parser.error("Prism checks require both --prism-dll and --prism-include, and a 64-bit DLL")
build = Path(args.build_dir).resolve()
if not args.session:
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
"--session", "--build-dir", str(build), "--reader", args.reader,
*(["--prism-dll", str(args.prism_dll.resolve()),
"--prism-include", str(args.prism_include.resolve())]
if args.prism_dll else []),
*(["--32-bit"] if args.is32Bit else [])]))
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
objectPath = "/" + interface.replace(".", "/")
xml = '<node><interface name="' + interface + '">'
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", ""),
("PresentAccessibleEvent", "sssssssiis")):
xml += '<method name="' + method + '">'
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
xml += '<arg type="b" direction="out"/></method>'
xml += '</interface></node>'
events = []
def record(connection, sender, path, iface, method, parameters, invocation):
if method != "PresentAccessibleEvent":
events.append((method, parameters.unpack()))
invocation.return_value(GLib.Variant("(b)", (True,)))
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus",
"RequestName", GLib.Variant("(su)", (interface, 0)), None,
Gio.DBusCallFlags.NONE, 1000, None)
with tempfile.TemporaryDirectory(prefix="wine-access-notifications-") as temporary:
tempPath = Path(temporary)
env = os.environ.copy()
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
fixture = tempPath / "notification-fixture.exe"
compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc"
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows", "-o", str(fixture),
*(["-DWINE_ACCESS_PRISM_FIXTURE", "-I" + str(args.prism_include.resolve())]
if args.prism_dll else []),
str(Path(__file__).with_name("notification_fixture.c")),
"-luser32", "-lole32", "-loleaut32", "-luiautomationcore", "-lshell32"], check=True)
helper = app = None
try:
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller",
"--reader", args.reader], env=env)
time.sleep(.5)
app = subprocess.Popen(["wine", str(fixture),
*(["Z:" + str(args.prism_dll.resolve()).replace("/", "\\")]
if args.prism_dll else [])], env=env)
deadline = time.monotonic() + 20
while app.poll() is None and time.monotonic() < deadline:
while GLib.MainContext.default().iteration(False):
pass
time.sleep(.005)
while GLib.MainContext.default().iteration(False):
pass
assert app.poll() == 0, f"notification fixture failed: {app.poll()}"
assert helper.poll() is None, "helper exited during notifications"
assert events == [
("SpeakText", ("Menu caf\u00e9 \U0001f600", False)),
("SpeakText", ("Second item", True)),
("CancelSpeech", ()),
("SpeakText", ("x" * 4094, False)),
], events
print(f"PASS: {32 if args.is32Bit else 64}-bit UIA text, interruption, cancellation, "
f"Unicode, bounded text, and background silence through {args.reader}")
finally:
for child in (app, helper):
if child is not None and child.poll() is None:
child.terminate()
child.wait(timeout=5)
subprocess.run(["wineserver", "-k"], env=env, timeout=5)