Make the process of adding an orca extension easier.
This commit is contained in:
1 parent
a8118c12cf
commit
417ee40dda
6 files changed
+199
-35
No files matched your search
@@ -123,6 +123,30 @@ WINEPREFIX=/path/to/prefix /path/to/cthulhu-wine-access.exe --reader orca
|
|||||||
| Cthulhu (default) | `org.stormux.Cthulhu1.Service` | `/org/stormux/Cthulhu1/Service` |
|
| Cthulhu (default) | `org.stormux.Cthulhu1.Service` | `/org/stormux/Cthulhu1/Service` |
|
||||||
| Orca | `org.gnome.Orca1.Service` | `/org/gnome/Orca1/Service` |
|
| Orca | `org.gnome.Orca1.Service` | `/org/gnome/Orca1/Service` |
|
||||||
|
|
||||||
|
An Orca user extension can expose the Wine methods on its own object and
|
||||||
|
interface, without adding them to Orca's core service. Override either field
|
||||||
|
with `--reader-path` and `--reader-interface`; omitted fields retain the
|
||||||
|
selected reader's defaults. For an extension class named `WineAccess`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
WINEPREFIX=/path/to/prefix /path/to/cthulhu-wine-access.exe --reader orca \
|
||||||
|
--reader-path /org/gnome/Orca1/Service/WineAccess \
|
||||||
|
--reader-interface org.gnome.Orca1.WineAccess
|
||||||
|
```
|
||||||
|
|
||||||
|
Both overrides accept validated D-Bus values and apply regardless of their
|
||||||
|
position relative to `--reader`. The bus name stays that of the selected
|
||||||
|
reader; overrides do not grant review access to another connection.
|
||||||
|
|
||||||
|
Before starting its hooks or controller server, the helper introspects the
|
||||||
|
selected object and interface and checks the required method signatures for
|
||||||
|
the enabled capabilities. A missing extension or incompatible interface
|
||||||
|
causes startup to fail with a diagnostic. Controller support requires
|
||||||
|
`SpeakText`, `CancelSpeech`, `BrailleMessage` and `SpeakSsml`; control reading
|
||||||
|
requires `PresentAccessibleEvent`. Review methods remain optional. The
|
||||||
|
interface must be published before launching the helper. This check runs at
|
||||||
|
startup; later request failures still follow the ordinary error handling.
|
||||||
|
|
||||||
Selection applies to controller requests, accessibility events, review
|
Selection applies to controller requests, accessibility events, review
|
||||||
registration and review-capture authorization. Only one helper runs per Wine
|
registration and review-capture authorization. Only one helper runs per Wine
|
||||||
desktop/prefix, so stop the existing helper before changing readers. There is
|
desktop/prefix, so stop the existing helper before changing readers. There is
|
||||||
|
|||||||
@@ -88,18 +88,6 @@ HWND combo_event_control(HWND window, DWORD event) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Winegcc uses 16-bit wchar_t; Linux libc's wcs* functions expect 32-bit
|
|
||||||
// wchar_t. Use Wine's Unicode APIs or explicit code-unit comparisons.
|
|
||||||
bool wide_contains(const wchar_t *text, const wchar_t *needle) {
|
|
||||||
for (; *text != L'\0'; ++text) {
|
|
||||||
const wchar_t *left = text;
|
|
||||||
const wchar_t *right = needle;
|
|
||||||
while (*right != L'\0' && *left == *right) { ++left; ++right; }
|
|
||||||
if (*right == L'\0') return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ControlDescription {
|
struct ControlDescription {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string role;
|
std::string role;
|
||||||
@@ -162,6 +150,59 @@ bool reader_is_available() {
|
|||||||
return owned == TRUE;
|
return owned == TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool reader_supports_protocol(bool controllerEnabled, bool dialogReaderEnabled) {
|
||||||
|
GError *error = nullptr;
|
||||||
|
GVariant *reply = g_dbus_connection_call_sync(
|
||||||
|
connection, readerEndpoint.busName, readerEndpoint.objectPath,
|
||||||
|
"org.freedesktop.DBus.Introspectable", "Introspect", nullptr,
|
||||||
|
G_VARIANT_TYPE("(s)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
||||||
|
&error);
|
||||||
|
if (error) g_error_free(error);
|
||||||
|
if (!reply) {
|
||||||
|
std::fprintf(stderr, "wine-access: reader object %s is unavailable\n",
|
||||||
|
readerEndpoint.objectPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char *xml = nullptr;
|
||||||
|
g_variant_get(reply, "(&s)", &xml);
|
||||||
|
GDBusNodeInfo *node = g_dbus_node_info_new_for_xml(xml, nullptr);
|
||||||
|
g_variant_unref(reply);
|
||||||
|
GDBusInterfaceInfo *interfaceInfo = node ?
|
||||||
|
g_dbus_node_info_lookup_interface(node, readerEndpoint.interfaceName) : nullptr;
|
||||||
|
struct RequiredMethod {
|
||||||
|
const char *name;
|
||||||
|
const char *inputs;
|
||||||
|
const char *outputs;
|
||||||
|
bool required;
|
||||||
|
};
|
||||||
|
const RequiredMethod methods[] = {
|
||||||
|
{"SpeakText", "sb", "b", controllerEnabled},
|
||||||
|
{"CancelSpeech", "", "b", controllerEnabled},
|
||||||
|
{"BrailleMessage", "s", "b", controllerEnabled},
|
||||||
|
{"SpeakSsml", "ss", "i", controllerEnabled},
|
||||||
|
{"PresentAccessibleEvent", "sssssssiis", "b", dialogReaderEnabled}};
|
||||||
|
bool supported = interfaceInfo != nullptr;
|
||||||
|
if (!interfaceInfo)
|
||||||
|
std::fprintf(stderr, "wine-access: reader interface %s is unavailable\n",
|
||||||
|
readerEndpoint.interfaceName);
|
||||||
|
for (const auto &required : methods) {
|
||||||
|
if (!interfaceInfo || !required.required) continue;
|
||||||
|
GDBusMethodInfo *method = g_dbus_interface_info_lookup_method(interfaceInfo, required.name);
|
||||||
|
std::string inputs, outputs;
|
||||||
|
if (method) {
|
||||||
|
for (auto args = method->in_args; args && *args; ++args) inputs += (*args)->signature;
|
||||||
|
for (auto args = method->out_args; args && *args; ++args) outputs += (*args)->signature;
|
||||||
|
}
|
||||||
|
if (!method || inputs != required.inputs || outputs != required.outputs) {
|
||||||
|
std::fprintf(stderr, "wine-access: reader method %s requires %s -> %s\n",
|
||||||
|
required.name, required.inputs, required.outputs);
|
||||||
|
supported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node) g_dbus_node_info_unref(node);
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
// Standard-control queries follow the LGPL wine-a11y reader published by
|
// Standard-control queries follow the LGPL wine-a11y reader published by
|
||||||
// Mudb0y, while presentation uses the selected reader's D-Bus interface.
|
// Mudb0y, while presentation uses the selected reader's D-Bus interface.
|
||||||
void strip_mnemonics(wchar_t *text) {
|
void strip_mnemonics(wchar_t *text) {
|
||||||
@@ -1008,7 +1049,32 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
wchar_t **arguments = CommandLineToArgvW(commandLine, &argumentCount);
|
wchar_t **arguments = CommandLineToArgvW(commandLine, &argumentCount);
|
||||||
if (arguments == nullptr) return 1;
|
if (arguments == nullptr) return 1;
|
||||||
bool validReader = true;
|
bool validReader = true;
|
||||||
|
bool validEndpoint = true;
|
||||||
|
std::string readerPath, readerInterface;
|
||||||
|
bool dialogReaderEnabled = true, controllerEnabled = true;
|
||||||
for (int index = 1; index < argumentCount; ++index) {
|
for (int index = 1; index < argumentCount; ++index) {
|
||||||
|
if (lstrcmpW(arguments[index], L"--no-dialog-reader") == 0) {
|
||||||
|
dialogReaderEnabled = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lstrcmpW(arguments[index], L"--no-controller") == 0) {
|
||||||
|
controllerEnabled = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool pathOption = lstrcmpW(arguments[index], L"--reader-path") == 0;
|
||||||
|
const bool interfaceOption = lstrcmpW(arguments[index], L"--reader-interface") == 0;
|
||||||
|
if (pathOption || interfaceOption) {
|
||||||
|
if (++index == argumentCount) { validEndpoint = false; break; }
|
||||||
|
std::string value = to_utf8(arguments[index]);
|
||||||
|
if (pathOption ? !g_variant_is_object_path(value.c_str()) :
|
||||||
|
!g_dbus_is_interface_name(value.c_str())) {
|
||||||
|
validEndpoint = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (pathOption) readerPath = value;
|
||||||
|
else readerInterface = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (lstrcmpW(arguments[index], L"--reader") != 0) continue;
|
if (lstrcmpW(arguments[index], L"--reader") != 0) continue;
|
||||||
if (++index == argumentCount) { validReader = false; break; }
|
if (++index == argumentCount) { validReader = false; break; }
|
||||||
if (lstrcmpW(arguments[index], L"cthulhu") == 0)
|
if (lstrcmpW(arguments[index], L"cthulhu") == 0)
|
||||||
@@ -1022,10 +1088,14 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
std::fprintf(stderr, "wine-access: --reader requires cthulhu or orca\n");
|
std::fprintf(stderr, "wine-access: --reader requires cthulhu or orca\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const bool dialogReaderEnabled =
|
if (!validEndpoint) {
|
||||||
!wide_contains(commandLine, L"--no-dialog-reader");
|
std::fprintf(stderr, "wine-access: --reader-path and --reader-interface require valid D-Bus values\n");
|
||||||
const bool controllerEnabled =
|
return 1;
|
||||||
!wide_contains(commandLine, L"--no-controller");
|
}
|
||||||
|
// Apply overrides after reader selection, independent of argument order.
|
||||||
|
// These strings remain alive until the workers have stopped at shutdown.
|
||||||
|
if (!readerPath.empty()) readerEndpoint.objectPath = readerPath.c_str();
|
||||||
|
if (!readerInterface.empty()) readerEndpoint.interfaceName = readerInterface.c_str();
|
||||||
if (!dialogReaderEnabled && !controllerEnabled)
|
if (!dialogReaderEnabled && !controllerEnabled)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -1050,6 +1120,12 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!reader_supports_protocol(controllerEnabled, dialogReaderEnabled)) {
|
||||||
|
g_object_unref(connection);
|
||||||
|
CloseHandle(singleton);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Application-owned MSAA objects (including Windows Forms menus) are
|
// Application-owned MSAA objects (including Windows Forms menus) are
|
||||||
// marshaled across processes. Initialize their callback/message-loop thread;
|
// marshaled across processes. Initialize their callback/message-loop thread;
|
||||||
// Wine's built-in control proxies can otherwise hide this missing setup.
|
// Wine's built-in control proxies can otherwise hide this missing setup.
|
||||||
|
|||||||
@@ -102,12 +102,29 @@ The controller test drives the helper through a native Windows NVDA RPC client.
|
|||||||
It verifies speech, cancellation, braille, SSML status forwarding, reader loss,
|
It verifies speech, cancellation, braille, SSML status forwarding, reader loss,
|
||||||
invalid reader values, and startup refusal when only the unselected service
|
invalid reader values, and startup refusal when only the unselected service
|
||||||
exists. Both service names are owned while the selected reader handles requests.
|
exists. Both service names are owned while the selected reader handles requests.
|
||||||
|
It also rejects invalid endpoint overrides, missing objects/interfaces,
|
||||||
|
missing required methods and incorrect input/output signatures.
|
||||||
The menu/editor and review tests also accept `--reader cthulhu`; omit the option
|
The menu/editor and review tests also accept `--reader cthulhu`; omit the option
|
||||||
to check existing default behavior. Review checks reject callers even when they
|
to check existing default behavior. Review checks reject callers even when they
|
||||||
own the unselected service, then replace the selected service owner and verify
|
own the unselected service, then replace the selected service owner and verify
|
||||||
registration and capture access move to the new owner. Its provider object path
|
registration and capture access move to the new owner. Its provider object path
|
||||||
and interface remain the same for both readers.
|
and interface remain the same for both readers.
|
||||||
|
|
||||||
|
To check an Orca extension endpoint, add `--extension-endpoint`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xvfb-run -a python3 tests/controller_integration.py --build-dir build --reader orca --extension-endpoint
|
||||||
|
xvfb-run -a python3 tests/menu_integration.py --build-dir build --reader orca --extension-endpoint
|
||||||
|
xvfb-run -a python3 tests/menu_integration.py --build-dir build --reader orca --extension-endpoint --fixture edit
|
||||||
|
xvfb-run -a python3 tests/review_integration.py --build-dir build --reader orca --extension-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
These publish `/org/gnome/Orca1/Service/WineAccess` with interface
|
||||||
|
`org.gnome.Orca1.WineAccess`, while retaining the `org.gnome.Orca1.Service`
|
||||||
|
bus name. Controller checks put overrides before reader selection; control
|
||||||
|
and review checks put them after it. Review checks still reject unrelated
|
||||||
|
callers and transfer capture access only when the selected bus owner changes.
|
||||||
|
|
||||||
These tests supply recording services implementing the Wine protocol; they do
|
These tests supply recording services implementing the Wine protocol; they do
|
||||||
not run Orca or prove audible speech or physical braille. Stock Orca still needs
|
not run Orca or prove audible speech or physical braille. Stock Orca still needs
|
||||||
the reader-side methods and helper lifecycle described in the
|
the reader-side methods and helper lifecycle described in the
|
||||||
|
|||||||
@@ -12,17 +12,21 @@ from gi.repository import Gio, GLib
|
|||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||||
|
parser.add_argument("--extension-endpoint", action="store_true")
|
||||||
parser.add_argument("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
if not args.session:
|
||||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||||
"--session", "--build-dir", str(build),
|
"--session", "--build-dir", str(build),
|
||||||
|
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
||||||
*(["--reader", args.reader] if args.reader else [])]))
|
*(["--reader", args.reader] if args.reader else [])]))
|
||||||
|
|
||||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||||
|
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
||||||
otherInterface = "org.stormux.Cthulhu1.Service" if args.reader == "orca" else "org.gnome.Orca1.Service"
|
otherInterface = "org.stormux.Cthulhu1.Service" if args.reader == "orca" else "org.gnome.Orca1.Service"
|
||||||
objectPath = "/" + interface.replace(".", "/")
|
objectPath = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_endpoint else "")
|
||||||
|
endpointArguments = ["--reader-path", objectPath, "--reader-interface", interface] if args.extension_endpoint else []
|
||||||
methods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
|
methods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
|
||||||
"BrailleMessage": ("s", "b"), "SpeakSsml": ("ss", "i")}
|
"BrailleMessage": ("s", "b"), "SpeakSsml": ("ss", "i")}
|
||||||
xml = '<node><interface name="' + interface + '">'
|
xml = '<node><interface name="' + interface + '">'
|
||||||
@@ -41,7 +45,7 @@ def record(connection, sender, path, iface, method, parameters, invocation):
|
|||||||
|
|
||||||
|
|
||||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
registration = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||||
|
|
||||||
|
|
||||||
def name_call(method, name):
|
def name_call(method, name):
|
||||||
@@ -51,13 +55,13 @@ def name_call(method, name):
|
|||||||
Gio.DBusCallFlags.NONE, 1000, None).unpack()[0]
|
Gio.DBusCallFlags.NONE, 1000, None).unpack()[0]
|
||||||
|
|
||||||
|
|
||||||
def wait_for_client(client):
|
def wait_for_client(client, expected=0):
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while client.poll() is None and time.monotonic() < deadline:
|
while client.poll() is None and time.monotonic() < deadline:
|
||||||
while GLib.MainContext.default().iteration(False):
|
while GLib.MainContext.default().iteration(False):
|
||||||
pass
|
pass
|
||||||
time.sleep(.005)
|
time.sleep(.005)
|
||||||
assert client.poll() == 0, f"controller client failed or timed out: {client.poll()}"
|
assert client.poll() == expected, f"client failed or timed out: {client.poll()}"
|
||||||
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||||
@@ -80,6 +84,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
|||||||
helper = None
|
helper = None
|
||||||
client = None
|
client = None
|
||||||
helperCommand = [str(build / "cthulhu-wine-access.exe"), "--no-dialog-reader",
|
helperCommand = [str(build / "cthulhu-wine-access.exe"), "--no-dialog-reader",
|
||||||
|
*endpointArguments,
|
||||||
*(["--reader", args.reader] if args.reader else [])]
|
*(["--reader", args.reader] if args.reader else [])]
|
||||||
try:
|
try:
|
||||||
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
|
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
|
||||||
@@ -87,11 +92,43 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
|||||||
assert name_call("RequestName", otherInterface) == 1
|
assert name_call("RequestName", otherInterface) == 1
|
||||||
result = subprocess.run(helperCommand, env=env, timeout=5)
|
result = subprocess.run(helperCommand, env=env, timeout=5)
|
||||||
assert result.returncode == 1, "helper accepted an unselected reader"
|
assert result.returncode == 1, "helper accepted an unselected reader"
|
||||||
for readerArgs in (["--reader"], ["--reader", "unknown"]):
|
for readerArgs in (["--reader"], ["--reader", "unknown"],
|
||||||
|
["--reader-path"], ["--reader-path", ""],
|
||||||
|
["--reader-path", "relative/path"], ["--reader-path", "/bad-path"],
|
||||||
|
["--reader-interface"], ["--reader-interface", ""],
|
||||||
|
["--reader-interface", "WineAccess"],
|
||||||
|
["--reader-interface", "org.example.Bad-Interface"]):
|
||||||
result = subprocess.run([str(build / "cthulhu-wine-access.exe"), *readerArgs],
|
result = subprocess.run([str(build / "cthulhu-wine-access.exe"), *readerArgs],
|
||||||
env=env, timeout=5)
|
env=env, timeout=5)
|
||||||
assert result.returncode == 1, "invalid reader argument was accepted"
|
assert result.returncode == 1, "invalid endpoint argument was accepted"
|
||||||
assert name_call("RequestName", interface) == 1
|
assert name_call("RequestName", busName) == 1
|
||||||
|
# An owned reader name is insufficient: refuse missing objects,
|
||||||
|
# interfaces, methods and incompatible signatures before starting RPC.
|
||||||
|
def reject(command):
|
||||||
|
rejected = subprocess.Popen(command, env=env, stderr=subprocess.PIPE)
|
||||||
|
try:
|
||||||
|
wait_for_client(rejected, 1)
|
||||||
|
return rejected.communicate()[1].decode()
|
||||||
|
finally:
|
||||||
|
if rejected.poll() is None:
|
||||||
|
rejected.kill()
|
||||||
|
rejected.wait()
|
||||||
|
diagnostic = reject([*helperCommand, "--reader-path", "/Missing"])
|
||||||
|
# Gio can introspect an absent object as an empty node.
|
||||||
|
assert "reader object" in diagnostic or "reader interface" in diagnostic, diagnostic
|
||||||
|
assert "reader interface" in reject([*helperCommand, "--reader-interface", "org.example.Missing"])
|
||||||
|
connection.unregister_object(registration)
|
||||||
|
for brokenXml in (
|
||||||
|
xml.replace('<method name="CancelSpeech"><arg type="b" direction="out"/></method>', ''),
|
||||||
|
xml.replace('<arg type="i" direction="out"/>', '<arg type="b" direction="out"/>'),
|
||||||
|
xml.replace('<arg type="b" direction="in"/>', '<arg type="s" direction="in"/>')):
|
||||||
|
broken = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(brokenXml).interfaces[0], record, None, None)
|
||||||
|
assert "reader method" in reject(helperCommand)
|
||||||
|
connection.unregister_object(broken)
|
||||||
|
registration = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||||
|
# Controller-only interfaces must not enable control reading.
|
||||||
|
assert "PresentAccessibleEvent" in reject([part for part in helperCommand if part != "--no-dialog-reader"])
|
||||||
|
print("PASS: endpoint validation and required method signatures")
|
||||||
helper = subprocess.Popen(helperCommand, env=env)
|
helper = subprocess.Popen(helperCommand, env=env)
|
||||||
# The fixture waits for the RPC server to listen before making calls.
|
# The fixture waits for the RPC server to listen before making calls.
|
||||||
client = subprocess.Popen(["wine", str(clientPath)], env=env)
|
client = subprocess.Popen(["wine", str(clientPath)], env=env)
|
||||||
@@ -103,7 +140,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
|||||||
("BrailleMessage", ("Controller braille",)),
|
("BrailleMessage", ("Controller braille",)),
|
||||||
("SpeakSsml", ("wine-nvda-controller", "<speak>Controller SSML</speak>")),
|
("SpeakSsml", ("wine-nvda-controller", "<speak>Controller SSML</speak>")),
|
||||||
], events
|
], events
|
||||||
assert name_call("ReleaseName", interface) == 1
|
assert name_call("ReleaseName", busName) == 1
|
||||||
client = subprocess.Popen(["wine", str(clientPath), "unavailable"], env=env)
|
client = subprocess.Popen(["wine", str(clientPath), "unavailable"], env=env)
|
||||||
wait_for_client(client)
|
wait_for_client(client)
|
||||||
print(f"PASS: {interface} controller routing, SSML status, service loss, and reader validation")
|
print(f"PASS: {interface} controller routing, SSML status, service loss, and reader validation")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ parser = argparse.ArgumentParser()
|
|||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||||
|
parser.add_argument("--extension-endpoint", action="store_true")
|
||||||
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
|
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
|
||||||
parser.add_argument("--notepadpp-exe", type=Path)
|
parser.add_argument("--notepadpp-exe", type=Path)
|
||||||
parser.add_argument("--goldwave-exe", type=Path)
|
parser.add_argument("--goldwave-exe", type=Path)
|
||||||
@@ -38,6 +39,7 @@ if not args.session:
|
|||||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
||||||
__file__, "--session", "--build-dir", str(build),
|
__file__, "--session", "--build-dir", str(build),
|
||||||
"--fixture", args.fixture,
|
"--fixture", args.fixture,
|
||||||
|
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
||||||
*(["--reader", args.reader] if args.reader else []),
|
*(["--reader", args.reader] if args.reader else []),
|
||||||
*(["--notepadpp-exe", str(args.notepadpp_exe.resolve())] if args.notepadpp_exe else []),
|
*(["--notepadpp-exe", str(args.notepadpp_exe.resolve())] if args.notepadpp_exe else []),
|
||||||
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
|
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
|
||||||
@@ -45,9 +47,12 @@ if not args.session:
|
|||||||
*(["--late-helper"] if args.late_helper else []),
|
*(["--late-helper"] if args.late_helper else []),
|
||||||
*(["--restart-helper"] if args.restart_helper else [])]))
|
*(["--restart-helper"] if args.restart_helper else [])]))
|
||||||
|
|
||||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||||
object_path = "/" + interface.replace(".", "/")
|
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
||||||
|
object_path = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_endpoint else "")
|
||||||
readerArguments = ["--reader", args.reader] if args.reader else []
|
readerArguments = ["--reader", args.reader] if args.reader else []
|
||||||
|
if args.extension_endpoint:
|
||||||
|
readerArguments += ["--reader-path", object_path, "--reader-interface", interface]
|
||||||
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
||||||
for index, kind in enumerate(["s"] * 7 + ["i", "i", "s"]):
|
for index, kind in enumerate(["s"] * 7 + ["i", "i", "s"]):
|
||||||
xml += f'<arg name="arg{index}" type="{kind}" direction="in"/>'
|
xml += f'<arg name="arg{index}" type="{kind}" direction="in"/>'
|
||||||
@@ -62,7 +67,7 @@ connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
|||||||
connection.register_object(object_path, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
connection.register_object(object_path, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||||
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||||
"org.freedesktop.DBus", "RequestName",
|
"org.freedesktop.DBus", "RequestName",
|
||||||
GLib.Variant("(su)", (interface, 0)), None,
|
GLib.Variant("(su)", (busName, 0)), None,
|
||||||
Gio.DBusCallFlags.NONE, 1000, None)
|
Gio.DBusCallFlags.NONE, 1000, None)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||||
|
|||||||
@@ -13,16 +13,21 @@ parser.add_argument('--build-dir', default='build')
|
|||||||
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
||||||
parser.add_argument('--session', action='store_true')
|
parser.add_argument('--session', action='store_true')
|
||||||
parser.add_argument('--reader', choices=('cthulhu', 'orca'))
|
parser.add_argument('--reader', choices=('cthulhu', 'orca'))
|
||||||
|
parser.add_argument('--extension-endpoint', action='store_true')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
if not args.session:
|
||||||
sys.exit(subprocess.call(['dbus-run-session', '--', sys.executable, __file__,
|
sys.exit(subprocess.call(['dbus-run-session', '--', sys.executable, __file__,
|
||||||
'--session', '--build-dir', str(build),
|
'--session', '--build-dir', str(build),
|
||||||
|
*(['--extension-endpoint'] if args.extension_endpoint else []),
|
||||||
*(['--reader', args.reader] if args.reader else []),
|
*(['--reader', args.reader] if args.reader else []),
|
||||||
*(['--32-bit'] if args.is32 else [])]))
|
*(['--32-bit'] if args.is32 else [])]))
|
||||||
iface = 'org.gnome.Orca1.Service' if args.reader == 'orca' else 'org.stormux.Cthulhu1.Service'
|
busName = 'org.gnome.Orca1.Service' if args.reader == 'orca' else 'org.stormux.Cthulhu1.Service'
|
||||||
path = '/' + iface.replace('.', '/')
|
iface = busName.rsplit('.', 1)[0] + '.WineAccess' if args.extension_endpoint else busName
|
||||||
|
path = '/' + busName.replace('.', '/') + ('/WineAccess' if args.extension_endpoint else '')
|
||||||
readerArguments = ['--reader', args.reader] if args.reader else []
|
readerArguments = ['--reader', args.reader] if args.reader else []
|
||||||
|
if args.extension_endpoint:
|
||||||
|
readerArguments += ['--reader-path', path, '--reader-interface', iface]
|
||||||
capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
||||||
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
||||||
|
|
||||||
@@ -50,7 +55,7 @@ def record(conn, sender, object_path, interface, method, params, invocation):
|
|||||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
registration = connection.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0], record, None, None)
|
registration = connection.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0], record, None, None)
|
||||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
GLib.Variant('(su)',(busName,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
|
|
||||||
def pump_until(predicate, seconds=5):
|
def pump_until(predicate, seconds=5):
|
||||||
end = time.monotonic()+seconds
|
end = time.monotonic()+seconds
|
||||||
@@ -127,9 +132,9 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
other.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0],record,None,None)
|
other.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0],record,None,None)
|
||||||
providers.clear(); focuses.clear()
|
providers.clear(); focuses.clear()
|
||||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
||||||
GLib.Variant('(s)',(iface,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
GLib.Variant('(s)',(busName,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
GLib.Variant('(su)',(busName,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
pump_until(lambda:sender in providers and sender in focuses)
|
pump_until(lambda:sender in providers and sender in focuses)
|
||||||
focus=focuses[sender]
|
focus=focuses[sender]
|
||||||
wrong=request(connection,sender,focus); pump_until(lambda:bool(wrong),2)
|
wrong=request(connection,sender,focus); pump_until(lambda:bool(wrong),2)
|
||||||
@@ -139,9 +144,9 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
assert accepted[0][0].unpack()[0]['status']=='ok'
|
assert accepted[0][0].unpack()[0]['status']=='ok'
|
||||||
providers.clear(); focuses.clear()
|
providers.clear(); focuses.clear()
|
||||||
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
||||||
GLib.Variant('(s)',(iface,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
GLib.Variant('(s)',(busName,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
GLib.Variant('(su)',(busName,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
pump_until(lambda:sender in providers and sender in focuses)
|
pump_until(lambda:sender in providers and sender in focuses)
|
||||||
other.close_sync(None)
|
other.close_sync(None)
|
||||||
print('PASS: test_reader_owner_replacement')
|
print('PASS: test_reader_owner_replacement')
|
||||||
|
|||||||
Reference in new issue
Block a user