Make d-bus available to orca if someone decides they want to implement it.
This commit is contained in:
1 parent
c3e981b4c2
commit
0f59809e9a
11 files changed
+374
-39
No files matched your search
+1
-1
@@ -54,7 +54,7 @@ add_dependencies(cthulhu-wine-access wine-access-menu wine-access-menu32)
|
|||||||
target_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
|
target_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
|
||||||
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
||||||
target_link_libraries(cthulhu-wine-access PRIVATE
|
target_link_libraries(cthulhu-wine-access PRIVATE
|
||||||
PkgConfig::GIO PkgConfig::XCB oleacc ole32 oleaut32 rpcrt4 user32 gdi32)
|
PkgConfig::GIO PkgConfig::XCB oleacc ole32 oleaut32 rpcrt4 user32 gdi32 shell32)
|
||||||
target_link_options(cthulhu-wine-access PRIVATE -mwindows)
|
target_link_options(cthulhu-wine-access PRIVATE -mwindows)
|
||||||
target_compile_options(cthulhu-wine-access PRIVATE
|
target_compile_options(cthulhu-wine-access PRIVATE
|
||||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wall -Wextra>)
|
$<$<COMPILE_LANGUAGE:C,CXX>:-Wall -Wextra>)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# Wine Access
|
# Wine Access
|
||||||
|
|
||||||
`wine-access` is the standalone Wine-hosted accessibility helper for Cthulhu
|
`wine-access` is the standalone Wine-hosted accessibility helper for Cthulhu
|
||||||
and other readers that implement the Cthulhu D-Bus service.
|
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 two independent capabilities:
|
||||||
|
|
||||||
@@ -12,7 +14,8 @@ It provides two independent capabilities:
|
|||||||
The compatibility window is hidden and non-activating. It is expected to be
|
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
|
invisible; the important endpoint is the local Wine RPC server registered by
|
||||||
the helper. The helper forwards speech, braille, cancellation, and standard
|
the helper. The helper forwards speech, braille, cancellation, and standard
|
||||||
control events to `org.stormux.Cthulhu1.Service` on the session D-Bus.
|
control events to the selected reader on the session D-Bus. The default is
|
||||||
|
`org.stormux.Cthulhu1.Service`.
|
||||||
|
|
||||||
The executable is named `cthulhu-wine-access.exe` for compatibility with the
|
The executable is named `cthulhu-wine-access.exe` for compatibility with the
|
||||||
Cthulhu-Go supervisor.
|
Cthulhu-Go supervisor.
|
||||||
@@ -86,8 +89,66 @@ $HOME/.local/libexec/cthulhu/wine/cthulhu-wine-access.exe
|
|||||||
```
|
```
|
||||||
|
|
||||||
The helper is started automatically when Cthulhu-Go detects an active Wine
|
The helper is started automatically when Cthulhu-Go detects an active Wine
|
||||||
loader for the same user and prefix. It requires the Go service to be running;
|
loader for the same user and prefix. It requires the selected reader service
|
||||||
it exits if `org.stormux.Cthulhu1.Service` is unavailable.
|
to be running; it exits if that service is unavailable.
|
||||||
|
|
||||||
|
## Reader selection and Orca integration
|
||||||
|
|
||||||
|
The default invocation and `--reader cthulhu` use the existing Cthulhu endpoint.
|
||||||
|
An Orca-side integration can launch the helper in the application's Wine prefix:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
WINEPREFIX=/path/to/prefix /path/to/cthulhu-wine-access.exe --reader orca
|
||||||
|
```
|
||||||
|
|
||||||
|
| Reader | Session bus name and interface | Object path |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Cthulhu (default) | `org.stormux.Cthulhu1.Service` | `/org/stormux/Cthulhu1/Service` |
|
||||||
|
| Orca | `org.gnome.Orca1.Service` | `/org/gnome/Orca1/Service` |
|
||||||
|
|
||||||
|
Selection applies to controller requests, accessibility events, review
|
||||||
|
registration and review-capture authorization. Only one helper runs per Wine
|
||||||
|
desktop/prefix, so stop the existing helper before changing readers. There is
|
||||||
|
no automatic switching between services. Unknown or missing reader values
|
||||||
|
are rejected. Orca must arrange helper startup and shutdown itself.
|
||||||
|
|
||||||
|
The Orca endpoint follows the versioned service in Orca's `dbus_service.py`.
|
||||||
|
It uses the same Wine methods and signatures as Cthulhu, on the selected
|
||||||
|
service interface:
|
||||||
|
|
||||||
|
| Method | Input signature | Output signature |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SpeakText` | `sb` (text, interrupt) | `b` |
|
||||||
|
| `CancelSpeech` | empty | `b` |
|
||||||
|
| `BrailleMessage` | `s` (text) | `b` |
|
||||||
|
| `SpeakSsml` | `ss` (source, SSML) | `i` (Windows status code) |
|
||||||
|
| `PresentAccessibleEvent` | `sssssssiis` | `b` |
|
||||||
|
| `RegisterWineReviewProvider` (optional) | `ou` (provider path, version) | `b` |
|
||||||
|
| `PresentWineReviewFocus` (optional) | `ssstssb(iiii)` | `b` |
|
||||||
|
|
||||||
|
Boolean replies indicate whether the reader accepted the request. The helper
|
||||||
|
queues plain controller speech with `interrupt=false`; Tolk requests
|
||||||
|
interruption separately through `CancelSpeech`. An unsupported `SpeakSsml`
|
||||||
|
implementation should return `ERROR_CALL_NOT_IMPLEMENTED` (120).
|
||||||
|
`PresentAccessibleEvent` arguments are source ID, event type, name, role,
|
||||||
|
value, states, description, position, count and window title, in that order.
|
||||||
|
The reader handles focus tracking, event filtering and presentation; preserve
|
||||||
|
repeated editor actions and exclude password values as described below.
|
||||||
|
|
||||||
|
Orca's existing `SpeakMessage` and `DisplayMessage` methods have different
|
||||||
|
names/signatures. Wine Access does not translate to those methods. An Orca
|
||||||
|
contributor must implement the Wine methods above for the capabilities they
|
||||||
|
want to enable. `--no-controller` and `--no-dialog-reader` can disable either
|
||||||
|
capability while bringing up the integration.
|
||||||
|
|
||||||
|
Review retains the shared `/org/stormux/Cthulhu1/WineReview` object path and
|
||||||
|
`org.stormux.Cthulhu1.WineReview` interface for both readers. Use the helper's
|
||||||
|
unique D-Bus sender from `RegisterWineReviewProvider` as the destination for
|
||||||
|
`CaptureWindow`. Only the current same-user owner of the selected reader
|
||||||
|
service may capture; owning the other reader name grants no access. See
|
||||||
|
[the flat-review contract](#independent-wine-flat-review) for focus arguments,
|
||||||
|
capture replies and limits. Missing review methods leave ordinary control and
|
||||||
|
controller forwarding available.
|
||||||
|
|
||||||
When a game closes inside a Wine virtual desktop, the desktop can regain X11
|
When a game closes inside a Wine virtual desktop, the desktop can regain X11
|
||||||
focus while Wine has no foreground application. The helper restores native
|
focus while Wine has no foreground application. The helper restores native
|
||||||
@@ -254,7 +315,7 @@ Protocol version 1 adds `RegisterWineReviewProvider(ou) -> b` and
|
|||||||
`PresentWineReviewFocus(ssstssb(iiii)) -> b` to the reader service. The helper
|
`PresentWineReviewFocus(ssstssb(iiii)) -> b` to the reader service. The helper
|
||||||
exports `/org/stormux/Cthulhu1/WineReview`, interface
|
exports `/org/stormux/Cthulhu1/WineReview`, interface
|
||||||
`org.stormux.Cthulhu1.WineReview`, with `CaptureWindow(sst) -> a{sv}`. Only the
|
`org.stormux.Cthulhu1.WineReview`, with `CaptureWindow(sst) -> a{sv}`. Only the
|
||||||
current same-user reader service owner can capture. Replies contain exactly
|
current same-user owner of the selected reader service can capture. Replies contain exactly
|
||||||
`version:u`, `instance:s`, `window:s`, `focus:s`, `revision:t`, `status:s`,
|
`version:u`, `instance:s`, `window:s`, `focus:s`, `revision:t`, `status:s`,
|
||||||
`bounds:(iiii)`, and `nodes:aa{sv}`. Nodes have `id`, `parent`, `role`, `name`
|
`bounds:(iiii)`, and `nodes:aa{sv}`. Nodes have `id`, `parent`, `role`, `name`
|
||||||
strings; `protected`, `visible`, `text_known`, `caret_known` booleans;
|
strings; `protected`, `visible`, `text_known`, `caret_known` booleans;
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
#include "native_items.h"
|
#include "native_items.h"
|
||||||
#include "review_service.h"
|
#include "review_service.h"
|
||||||
#include "foreground_focus.h"
|
#include "foreground_focus.h"
|
||||||
|
#include "reader_endpoint.h"
|
||||||
|
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
#include <oleacc.h>
|
#include <oleacc.h>
|
||||||
#include <rpc.h>
|
#include <rpc.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
@@ -25,9 +27,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr auto BUS_NAME = "org.stormux.Cthulhu1.Service";
|
ReaderEndpoint readerEndpoint = CthulhuEndpoint;
|
||||||
constexpr auto OBJECT_PATH = "/org/stormux/Cthulhu1/Service";
|
|
||||||
constexpr auto INTERFACE_NAME = "org.stormux.Cthulhu1.Service";
|
|
||||||
constexpr int DBUS_TIMEOUT_MS = 500;
|
constexpr int DBUS_TIMEOUT_MS = 500;
|
||||||
GDBusConnection *connection = nullptr;
|
GDBusConnection *connection = nullptr;
|
||||||
ReviewIdentityStore reviewIdentities;
|
ReviewIdentityStore reviewIdentities;
|
||||||
@@ -112,7 +112,8 @@ DWORD call_boolean(const char *method, GVariant *parameters) {
|
|||||||
return ERROR_SERVICE_NOT_ACTIVE;
|
return ERROR_SERVICE_NOT_ACTIVE;
|
||||||
GError *error = nullptr;
|
GError *error = nullptr;
|
||||||
GVariant *reply = g_dbus_connection_call_sync(
|
GVariant *reply = g_dbus_connection_call_sync(
|
||||||
connection, BUS_NAME, OBJECT_PATH, INTERFACE_NAME, method, parameters,
|
connection, readerEndpoint.busName, readerEndpoint.objectPath,
|
||||||
|
readerEndpoint.interfaceName, method, parameters,
|
||||||
G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
||||||
&error);
|
&error);
|
||||||
if (error != nullptr)
|
if (error != nullptr)
|
||||||
@@ -125,11 +126,12 @@ DWORD call_boolean(const char *method, GVariant *parameters) {
|
|||||||
return result == TRUE ? ERROR_SUCCESS : ERROR_GEN_FAILURE;
|
return result == TRUE ? ERROR_SUCCESS : ERROR_GEN_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool cthulhu_is_available() {
|
bool reader_is_available() {
|
||||||
GError *error = nullptr;
|
GError *error = nullptr;
|
||||||
GVariant *reply = g_dbus_connection_call_sync(
|
GVariant *reply = g_dbus_connection_call_sync(
|
||||||
connection, "org.freedesktop.DBus", "/org/freedesktop/DBus",
|
connection, "org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||||
"org.freedesktop.DBus", "NameHasOwner", g_variant_new("(s)", BUS_NAME),
|
"org.freedesktop.DBus", "NameHasOwner",
|
||||||
|
g_variant_new("(s)", readerEndpoint.busName),
|
||||||
G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
||||||
&error);
|
&error);
|
||||||
if (error != nullptr)
|
if (error != nullptr)
|
||||||
@@ -168,7 +170,7 @@ std::string accessible_property(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 remains on Cthulhu'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) {
|
||||||
wchar_t *output = text;
|
wchar_t *output = text;
|
||||||
for (wchar_t *input = text; *input != L'\0'; ++input) {
|
for (wchar_t *input = text; *input != L'\0'; ++input) {
|
||||||
@@ -895,7 +897,7 @@ extern "C" void *__RPC_USER MIDL_user_allocate(size_t size) {
|
|||||||
extern "C" void __RPC_USER MIDL_user_free(void *pointer) { free(pointer); }
|
extern "C" void __RPC_USER MIDL_user_free(void *pointer) { free(pointer); }
|
||||||
|
|
||||||
extern "C" error_status_t __stdcall testIfRunning(void) {
|
extern "C" error_status_t __stdcall testIfRunning(void) {
|
||||||
return cthulhu_is_available() ? ERROR_SUCCESS : ERROR_SERVICE_NOT_ACTIVE;
|
return reader_is_available() ? ERROR_SUCCESS : ERROR_SERVICE_NOT_ACTIVE;
|
||||||
}
|
}
|
||||||
extern "C" error_status_t __stdcall speakText(const wchar_t *text) {
|
extern "C" error_status_t __stdcall speakText(const wchar_t *text) {
|
||||||
const std::string utf8 = to_utf8(text);
|
const std::string utf8 = to_utf8(text);
|
||||||
@@ -920,7 +922,8 @@ extern "C" error_status_t __stdcall speakSsml(const wchar_t *ssml, SYMBOL_LEVEL,
|
|||||||
const std::string utf8 = to_utf8(ssml);
|
const std::string utf8 = to_utf8(ssml);
|
||||||
GError *error = nullptr;
|
GError *error = nullptr;
|
||||||
GVariant *reply = g_dbus_connection_call_sync(
|
GVariant *reply = g_dbus_connection_call_sync(
|
||||||
connection, BUS_NAME, OBJECT_PATH, INTERFACE_NAME, "SpeakSsml",
|
connection, readerEndpoint.busName, readerEndpoint.objectPath,
|
||||||
|
readerEndpoint.interfaceName, "SpeakSsml",
|
||||||
g_variant_new("(ss)", "wine-nvda-controller", utf8.c_str()),
|
g_variant_new("(ss)", "wine-nvda-controller", utf8.c_str()),
|
||||||
G_VARIANT_TYPE("(i)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
G_VARIANT_TYPE("(i)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
||||||
&error);
|
&error);
|
||||||
@@ -945,6 +948,24 @@ extern "C" error_status_t __stdcall isSpeaking(boolean *speaking) {
|
|||||||
|
|
||||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||||
const wchar_t *commandLine = GetCommandLineW();
|
const wchar_t *commandLine = GetCommandLineW();
|
||||||
|
int argumentCount = 0;
|
||||||
|
wchar_t **arguments = CommandLineToArgvW(commandLine, &argumentCount);
|
||||||
|
if (arguments == nullptr) return 1;
|
||||||
|
bool validReader = true;
|
||||||
|
for (int index = 1; index < argumentCount; ++index) {
|
||||||
|
if (lstrcmpW(arguments[index], L"--reader") != 0) continue;
|
||||||
|
if (++index == argumentCount) { validReader = false; break; }
|
||||||
|
if (lstrcmpW(arguments[index], L"cthulhu") == 0)
|
||||||
|
readerEndpoint = CthulhuEndpoint;
|
||||||
|
else if (lstrcmpW(arguments[index], L"orca") == 0)
|
||||||
|
readerEndpoint = OrcaEndpoint;
|
||||||
|
else { validReader = false; break; }
|
||||||
|
}
|
||||||
|
LocalFree(arguments);
|
||||||
|
if (!validReader) {
|
||||||
|
std::fprintf(stderr, "wine-access: --reader requires cthulhu or orca\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
const bool dialogReaderEnabled =
|
const bool dialogReaderEnabled =
|
||||||
!wide_contains(commandLine, L"--no-dialog-reader");
|
!wide_contains(commandLine, L"--no-dialog-reader");
|
||||||
const bool controllerEnabled =
|
const bool controllerEnabled =
|
||||||
@@ -965,7 +986,10 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
|
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
|
||||||
if (error != nullptr)
|
if (error != nullptr)
|
||||||
g_error_free(error);
|
g_error_free(error);
|
||||||
if (connection == nullptr || !cthulhu_is_available()) {
|
if (connection == nullptr || !reader_is_available()) {
|
||||||
|
std::fprintf(stderr, "wine-access: reader service %s is unavailable\n",
|
||||||
|
readerEndpoint.busName);
|
||||||
|
if (connection != nullptr) g_object_unref(connection);
|
||||||
CloseHandle(singleton);
|
CloseHandle(singleton);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -983,7 +1007,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HWND window = nullptr;
|
HWND window = nullptr;
|
||||||
reviewService.start(connection,reviewIdentities);
|
reviewService.start(connection,reviewIdentities,readerEndpoint);
|
||||||
bool controllerActive = false;
|
bool controllerActive = false;
|
||||||
if (controllerEnabled &&
|
if (controllerEnabled &&
|
||||||
FindWindowW(L"wxWindowClassNR", L"NVDA") == nullptr) {
|
FindWindowW(L"wxWindowClassNR", L"NVDA") == nullptr) {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
struct ReaderEndpoint {
|
||||||
|
const char *busName;
|
||||||
|
const char *objectPath;
|
||||||
|
const char *interfaceName;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr ReaderEndpoint CthulhuEndpoint{
|
||||||
|
"org.stormux.Cthulhu1.Service", "/org/stormux/Cthulhu1/Service",
|
||||||
|
"org.stormux.Cthulhu1.Service"};
|
||||||
|
inline constexpr ReaderEndpoint OrcaEndpoint{
|
||||||
|
"org.gnome.Orca1.Service", "/org/gnome/Orca1/Service",
|
||||||
|
"org.gnome.Orca1.Service"};
|
||||||
+9
-10
@@ -8,8 +8,6 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char *reader = "org.stormux.Cthulhu1.Service";
|
|
||||||
constexpr const char *reader_path = "/org/stormux/Cthulhu1/Service";
|
|
||||||
constexpr const char *path = "/org/stormux/Cthulhu1/WineReview";
|
constexpr const char *path = "/org/stormux/Cthulhu1/WineReview";
|
||||||
constexpr const char *xml = R"(<node><interface name="org.stormux.Cthulhu1.WineReview">
|
constexpr const char *xml = R"(<node><interface name="org.stormux.Cthulhu1.WineReview">
|
||||||
<method name="CaptureWindow"><arg type="s" direction="in"/><arg type="s" direction="in"/>
|
<method name="CaptureWindow"><arg type="s" direction="in"/><arg type="s" direction="in"/>
|
||||||
@@ -72,13 +70,14 @@ struct Job {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
struct ReviewService::State {
|
struct ReviewService::State {
|
||||||
explicit State(GDBusConnection *c,ReviewIdentityStore &s):connection(G_DBUS_CONNECTION(g_object_ref(c))),store(s) {
|
explicit State(GDBusConnection *c,ReviewIdentityStore &s,const ReaderEndpoint &reader):connection(G_DBUS_CONNECTION(g_object_ref(c))),store(s),endpoint(reader) {
|
||||||
context=g_main_context_new(); loop=g_main_loop_new(context,FALSE);
|
context=g_main_context_new(); loop=g_main_loop_new(context,FALSE);
|
||||||
calls=g_cancellable_new();
|
calls=g_cancellable_new();
|
||||||
}
|
}
|
||||||
~State() { g_object_unref(calls); g_main_loop_unref(loop); g_main_context_unref(context); g_object_unref(connection); }
|
~State() { g_object_unref(calls); g_main_loop_unref(loop); g_main_context_unref(context); g_object_unref(connection); }
|
||||||
GDBusConnection *connection;
|
GDBusConnection *connection;
|
||||||
ReviewIdentityStore store;
|
ReviewIdentityStore store;
|
||||||
|
const ReaderEndpoint endpoint;
|
||||||
GMainContext *context;
|
GMainContext *context;
|
||||||
GMainLoop *loop;
|
GMainLoop *loop;
|
||||||
GCancellable *calls;
|
GCancellable *calls;
|
||||||
@@ -106,7 +105,7 @@ struct ReviewService::State {
|
|||||||
std::string lookup_owner() {
|
std::string lookup_owner() {
|
||||||
GError *error=nullptr;
|
GError *error=nullptr;
|
||||||
GVariant *value=g_dbus_connection_call_sync(connection,"org.freedesktop.DBus","/org/freedesktop/DBus",
|
GVariant *value=g_dbus_connection_call_sync(connection,"org.freedesktop.DBus","/org/freedesktop/DBus",
|
||||||
"org.freedesktop.DBus","GetNameOwner",g_variant_new("(s)",reader),G_VARIANT_TYPE("(s)"),
|
"org.freedesktop.DBus","GetNameOwner",g_variant_new("(s)",endpoint.busName),G_VARIANT_TYPE("(s)"),
|
||||||
G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error);
|
G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error);
|
||||||
std::string result;
|
std::string result;
|
||||||
if (value) { const gchar *name=nullptr; g_variant_get(value,"(&s)",&name); result=name; g_variant_unref(value); }
|
if (value) { const gchar *name=nullptr; g_variant_get(value,"(&s)",&name); result=name; g_variant_unref(value); }
|
||||||
@@ -145,7 +144,7 @@ struct ReviewService::State {
|
|||||||
if (now<next_registration) return;
|
if (now<next_registration) return;
|
||||||
next_registration=now+1000;
|
next_registration=now+1000;
|
||||||
GError *error=nullptr;
|
GError *error=nullptr;
|
||||||
GVariant *value=g_dbus_connection_call_sync(connection,owner.c_str(),reader_path,reader,"RegisterWineReviewProvider",
|
GVariant *value=g_dbus_connection_call_sync(connection,owner.c_str(),endpoint.objectPath,endpoint.interfaceName,"RegisterWineReviewProvider",
|
||||||
g_variant_new("(ou)",path,1u),G_VARIANT_TYPE("(b)"),G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error);
|
g_variant_new("(ou)",path,1u),G_VARIANT_TYPE("(b)"),G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error);
|
||||||
gboolean accepted=FALSE;
|
gboolean accepted=FALSE;
|
||||||
if (value) { g_variant_get(value,"(b)",&accepted); g_variant_unref(value); }
|
if (value) { g_variant_get(value,"(b)",&accepted); g_variant_unref(value); }
|
||||||
@@ -156,7 +155,7 @@ struct ReviewService::State {
|
|||||||
{ std::lock_guard<std::mutex> lock(mutex); notice=focus; }
|
{ std::lock_guard<std::mutex> lock(mutex); notice=focus; }
|
||||||
if (!registered || notice.focus.empty() || same_notice(notice,sent)) return;
|
if (!registered || notice.focus.empty() || same_notice(notice,sent)) return;
|
||||||
sent=notice; announcing=true;
|
sent=notice; announcing=true;
|
||||||
g_dbus_connection_call(connection,owner.c_str(),reader_path,reader,"PresentWineReviewFocus",
|
g_dbus_connection_call(connection,owner.c_str(),endpoint.objectPath,endpoint.interfaceName,"PresentWineReviewFocus",
|
||||||
g_variant_new("(ssstssb(iiii))",notice.instance.c_str(),notice.window.c_str(),notice.focus.c_str(),notice.revision,
|
g_variant_new("(ssstssb(iiii))",notice.instance.c_str(),notice.window.c_str(),notice.focus.c_str(),notice.revision,
|
||||||
notice.title.c_str(),notice.role.c_str(),notice.protected_text,notice.bounds.left,notice.bounds.top,
|
notice.title.c_str(),notice.role.c_str(),notice.protected_text,notice.bounds.left,notice.bounds.top,
|
||||||
notice.bounds.right-notice.bounds.left,notice.bounds.bottom-notice.bounds.top),
|
notice.bounds.right-notice.bounds.left,notice.bounds.bottom-notice.bounds.top),
|
||||||
@@ -167,7 +166,7 @@ struct ReviewService::State {
|
|||||||
auto &state=*static_cast<State*>(data);
|
auto &state=*static_cast<State*>(data);
|
||||||
const uint64_t received=GetTickCount64();
|
const uint64_t received=GetTickCount64();
|
||||||
if (!state.same_user || state.owner!=sender || state.lookup_owner()!=sender) {
|
if (!state.same_user || state.owner!=sender || state.lookup_owner()!=sender) {
|
||||||
g_dbus_method_invocation_return_dbus_error(invocation,"org.freedesktop.DBus.Error.AccessDenied","Only the current Cthulhu service may capture Wine review."); return;
|
g_dbus_method_invocation_return_dbus_error(invocation,"org.freedesktop.DBus.Error.AccessDenied","Only the current selected reader service may capture Wine review."); return;
|
||||||
}
|
}
|
||||||
const char *window=nullptr,*focus=nullptr; guint64 revision=0;
|
const char *window=nullptr,*focus=nullptr; guint64 revision=0;
|
||||||
g_variant_get(parameters,"(&s&st)",&window,&focus,&revision);
|
g_variant_get(parameters,"(&s&st)",&window,&focus,&revision);
|
||||||
@@ -224,9 +223,9 @@ DWORD WINAPI ReviewService::State::work(LPVOID pointer) {
|
|||||||
{ std::lock_guard<std::mutex> lock(state->mutex); state->worker_done=true; state->changed.notify_all(); }
|
{ std::lock_guard<std::mutex> lock(state->mutex); state->worker_done=true; state->changed.notify_all(); }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
bool ReviewService::start(GDBusConnection *connection,ReviewIdentityStore &store) {
|
bool ReviewService::start(GDBusConnection *connection,ReviewIdentityStore &store,const ReaderEndpoint &endpoint) {
|
||||||
if (state_) return false;
|
if (state_) return false;
|
||||||
state_=std::make_shared<State>(connection,store);
|
state_=std::make_shared<State>(connection,store,endpoint);
|
||||||
auto state=state_;
|
auto state=state_;
|
||||||
auto worker_state=new std::shared_ptr<State>(state);
|
auto worker_state=new std::shared_ptr<State>(state);
|
||||||
worker_=CreateThread(nullptr,0,State::work,worker_state,0,nullptr);
|
worker_=CreateThread(nullptr,0,State::work,worker_state,0,nullptr);
|
||||||
@@ -267,7 +266,7 @@ DWORD WINAPI ReviewService::State::dispatch(LPVOID pointer) {
|
|||||||
},new std::shared_ptr<State>(state),[](gpointer p) { delete static_cast<std::shared_ptr<State>*>(p); });
|
},new std::shared_ptr<State>(state),[](gpointer p) { delete static_cast<std::shared_ptr<State>*>(p); });
|
||||||
g_source_attach(tick,state->context);
|
g_source_attach(tick,state->context);
|
||||||
state->owner_subscription=g_dbus_connection_signal_subscribe(state->connection,"org.freedesktop.DBus","org.freedesktop.DBus",
|
state->owner_subscription=g_dbus_connection_signal_subscribe(state->connection,"org.freedesktop.DBus","org.freedesktop.DBus",
|
||||||
"NameOwnerChanged","/org/freedesktop/DBus",reader,G_DBUS_SIGNAL_FLAGS_NONE,
|
"NameOwnerChanged","/org/freedesktop/DBus",state->endpoint.busName,G_DBUS_SIGNAL_FLAGS_NONE,
|
||||||
[](GDBusConnection*,const gchar*,const gchar*,const gchar*,const gchar*,GVariant*,gpointer p) {
|
[](GDBusConnection*,const gchar*,const gchar*,const gchar*,const gchar*,GVariant*,gpointer p) {
|
||||||
static_cast<State*>(p)->refresh_owner();
|
static_cast<State*>(p)->refresh_owner();
|
||||||
},state.get(),nullptr);
|
},state.get(),nullptr);
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "review_capture.h"
|
#include "review_capture.h"
|
||||||
|
#include "reader_endpoint.h"
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ class ReviewService {
|
|||||||
public:
|
public:
|
||||||
ReviewService() = default;
|
ReviewService() = default;
|
||||||
~ReviewService();
|
~ReviewService();
|
||||||
bool start(GDBusConnection *, ReviewIdentityStore &);
|
bool start(GDBusConnection *, ReviewIdentityStore &, const ReaderEndpoint &);
|
||||||
void focus_changed(const ReviewFocus &);
|
void focus_changed(const ReviewFocus &);
|
||||||
void stop();
|
void stop();
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
# Wine control integration
|
# Wine control integration
|
||||||
|
|
||||||
|
## Reader endpoint selection
|
||||||
|
|
||||||
|
After building, run these checks sequentially on private displays:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xvfb-run -a python3 tests/controller_integration.py --build-dir build
|
||||||
|
xvfb-run -a python3 tests/controller_integration.py --build-dir build --reader orca
|
||||||
|
xvfb-run -a python3 tests/menu_integration.py --build-dir build --reader orca
|
||||||
|
xvfb-run -a python3 tests/menu_integration.py --build-dir build --reader orca --fixture edit
|
||||||
|
xvfb-run -a python3 tests/review_integration.py --build-dir build --reader orca
|
||||||
|
```
|
||||||
|
|
||||||
|
The controller test drives the helper through a native Windows NVDA RPC client.
|
||||||
|
It verifies speech, cancellation, braille, SSML status forwarding, reader loss,
|
||||||
|
invalid reader values, and startup refusal when only the unselected service
|
||||||
|
exists. Both service names are owned while the selected reader handles requests.
|
||||||
|
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
|
||||||
|
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
|
||||||
|
and interface remain the same for both readers.
|
||||||
|
|
||||||
|
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
|
||||||
|
the reader-side methods and helper lifecycle described in the
|
||||||
|
[integration contract](../README.md#reader-selection-and-orca-integration).
|
||||||
|
Before submitting, check Cthulhu's ordinary Wine menus, editor arrows, repeated
|
||||||
|
characters, selections, deletion, protected fields and keypad review with live
|
||||||
|
speech and braille. When an Orca integration exists, repeat those workflows
|
||||||
|
with `--reader orca`, including return to Linux controls and reader restart.
|
||||||
|
|
||||||
## Virtual desktop focus recovery
|
## Virtual desktop focus recovery
|
||||||
|
|
||||||
`xvfb-run -a ctest --test-dir build -R foreground-focus --output-on-failure`
|
`xvfb-run -a ctest --test-dir build -R foreground-focus --output-on-failure`
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <rpc.h>
|
||||||
|
|
||||||
|
// The test client IDL supplies an explicit local binding; its wire interfaces
|
||||||
|
// and method signatures are otherwise identical to the helper's IDL.
|
||||||
|
#include "nvdaController_c.c"
|
||||||
|
|
||||||
|
void *__RPC_USER MIDL_user_allocate(size_t size) { return malloc(size); }
|
||||||
|
void __RPC_USER MIDL_user_free(void *pointer) { free(pointer); }
|
||||||
|
|
||||||
|
static int check_status(const char *operation, error_status_t status) {
|
||||||
|
if (status == ERROR_SUCCESS) return 0;
|
||||||
|
fprintf(stderr, "%s failed: %lu\n", operation, (unsigned long)status);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
RPC_WSTR bindingText = NULL;
|
||||||
|
RPC_BINDING_HANDLE binding = NULL;
|
||||||
|
if (RpcStringBindingComposeW(NULL, (RPC_WSTR)L"ncalrpc", NULL,
|
||||||
|
(RPC_WSTR)L"NvdaCtlr", NULL, &bindingText) ||
|
||||||
|
RpcBindingFromStringBindingW(bindingText, &binding))
|
||||||
|
return 1;
|
||||||
|
RpcStringFreeW(&bindingText);
|
||||||
|
controllerBinding = binding;
|
||||||
|
ULONGLONG deadline = GetTickCount64() + 5000;
|
||||||
|
while (RpcMgmtIsServerListening(binding) != RPC_S_OK) {
|
||||||
|
if (GetTickCount64() >= deadline) {
|
||||||
|
fprintf(stderr, "controller RPC server did not become ready\n");
|
||||||
|
RpcBindingFree(&binding);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
int failed = 0;
|
||||||
|
if (argc > 1 && strcmp(argv[1], "unavailable") == 0) {
|
||||||
|
failed = testIfRunning() != ERROR_SERVICE_NOT_ACTIVE;
|
||||||
|
} else {
|
||||||
|
failed |= check_status("testIfRunning", testIfRunning());
|
||||||
|
failed |= check_status("speakText", speakText(L"Controller speech"));
|
||||||
|
failed |= check_status("cancelSpeech", cancelSpeech());
|
||||||
|
failed |= check_status("brailleMessage", brailleMessage(L"Controller braille"));
|
||||||
|
// The reader's SSML status must reach the caller without conversion.
|
||||||
|
failed |= speakSsml(L"<speak>Controller SSML</speak>", SYMBOL_LEVEL_ALL,
|
||||||
|
SPEECH_PRIORITY_NORMAL, FALSE) != ERROR_CALL_NOT_IMPLEMENTED;
|
||||||
|
}
|
||||||
|
RpcBindingFree(&binding);
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Check NVDA RPC routing against private reader D-Bus services."""
|
||||||
|
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"))
|
||||||
|
parser.add_argument("--session", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
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] if args.reader else [])]))
|
||||||
|
|
||||||
|
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||||
|
otherInterface = "org.stormux.Cthulhu1.Service" if args.reader == "orca" else "org.gnome.Orca1.Service"
|
||||||
|
objectPath = "/" + interface.replace(".", "/")
|
||||||
|
methods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
|
||||||
|
"BrailleMessage": ("s", "b"), "SpeakSsml": ("ss", "i")}
|
||||||
|
xml = '<node><interface name="' + interface + '">'
|
||||||
|
for method, (inputs, output) in methods.items():
|
||||||
|
xml += '<method name="' + method + '">'
|
||||||
|
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
||||||
|
xml += '<arg type="' + output + '" direction="out"/></method>'
|
||||||
|
xml += '</interface></node>'
|
||||||
|
events = []
|
||||||
|
|
||||||
|
|
||||||
|
def record(connection, sender, path, iface, method, parameters, invocation):
|
||||||
|
events.append((method, parameters.unpack()))
|
||||||
|
invocation.return_value(GLib.Variant("(i)", (120,)) if method == "SpeakSsml"
|
||||||
|
else 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)
|
||||||
|
|
||||||
|
|
||||||
|
def name_call(method, name):
|
||||||
|
parameters = GLib.Variant("(su)", (name, 0)) if method == "RequestName" else GLib.Variant("(s)", (name,))
|
||||||
|
return connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||||
|
"org.freedesktop.DBus", method, parameters, None,
|
||||||
|
Gio.DBusCallFlags.NONE, 1000, None).unpack()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_client(client):
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
while client.poll() is None and time.monotonic() < deadline:
|
||||||
|
while GLib.MainContext.default().iteration(False):
|
||||||
|
pass
|
||||||
|
time.sleep(.005)
|
||||||
|
assert client.poll() == 0, f"controller client failed or timed out: {client.poll()}"
|
||||||
|
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="wine-access-controller-") 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")
|
||||||
|
source = Path(__file__).resolve().parent.parent
|
||||||
|
# Like NVDA's clients, bind directly to the local endpoint. WIDL's default
|
||||||
|
# automatic binding is unsupported by Wine's client NDR interpreter.
|
||||||
|
clientIdl = tempPath / "nvdaController.idl"
|
||||||
|
clientIdl.write_text((source / "nvdaController.idl").read_text().replace(
|
||||||
|
"\n uuid(", "\n implicit_handle(handle_t controllerBinding),\n uuid("))
|
||||||
|
subprocess.run(["widl", "--win64", "-c", "-o", str(tempPath / "nvdaController_c.c"),
|
||||||
|
str(clientIdl)], check=True)
|
||||||
|
clientPath = tempPath / "controller-fixture.exe"
|
||||||
|
subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-I" + str(tempPath),
|
||||||
|
"-I" + str(build / "generated"), "-o", str(clientPath),
|
||||||
|
str(source / "tests/controller_fixture.c"), "-lrpcrt4"], check=True)
|
||||||
|
helper = None
|
||||||
|
client = None
|
||||||
|
helperCommand = [str(build / "cthulhu-wine-access.exe"), "--no-dialog-reader",
|
||||||
|
*(["--reader", args.reader] if args.reader else [])]
|
||||||
|
try:
|
||||||
|
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
|
||||||
|
# Even when the other reader exists, the selected reader is required.
|
||||||
|
assert name_call("RequestName", otherInterface) == 1
|
||||||
|
result = subprocess.run(helperCommand, env=env, timeout=5)
|
||||||
|
assert result.returncode == 1, "helper accepted an unselected reader"
|
||||||
|
for readerArgs in (["--reader"], ["--reader", "unknown"]):
|
||||||
|
result = subprocess.run([str(build / "cthulhu-wine-access.exe"), *readerArgs],
|
||||||
|
env=env, timeout=5)
|
||||||
|
assert result.returncode == 1, "invalid reader argument was accepted"
|
||||||
|
assert name_call("RequestName", interface) == 1
|
||||||
|
helper = subprocess.Popen(helperCommand, env=env)
|
||||||
|
# The fixture waits for the RPC server to listen before making calls.
|
||||||
|
client = subprocess.Popen(["wine", str(clientPath)], env=env)
|
||||||
|
wait_for_client(client)
|
||||||
|
assert helper.poll() is None, "helper exited during controller requests"
|
||||||
|
assert events == [
|
||||||
|
("SpeakText", ("Controller speech", False)),
|
||||||
|
("CancelSpeech", ()),
|
||||||
|
("BrailleMessage", ("Controller braille",)),
|
||||||
|
("SpeakSsml", ("wine-nvda-controller", "<speak>Controller SSML</speak>")),
|
||||||
|
], events
|
||||||
|
assert name_call("ReleaseName", interface) == 1
|
||||||
|
client = subprocess.Popen(["wine", str(clientPath), "unavailable"], env=env)
|
||||||
|
wait_for_client(client)
|
||||||
|
print(f"PASS: {interface} controller routing, SSML status, service loss, and reader validation")
|
||||||
|
finally:
|
||||||
|
for child in (client, helper):
|
||||||
|
if child is not None and child.poll() is None:
|
||||||
|
child.terminate()
|
||||||
|
try:
|
||||||
|
child.wait(timeout=3)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
child.kill()
|
||||||
|
child.wait(timeout=3)
|
||||||
|
subprocess.run(["wineserver", "-k"], env=env, timeout=5)
|
||||||
@@ -17,6 +17,7 @@ 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("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
|
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||||
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
|
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "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)
|
||||||
@@ -37,14 +38,16 @@ 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,
|
||||||
|
*(["--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 []),
|
||||||
*(["--32-bit"] if args.is32Bit else []),
|
*(["--32-bit"] if args.is32Bit else []),
|
||||||
*(["--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.stormux.Cthulhu1.Service"
|
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||||
object_path = "/org/stormux/Cthulhu1/Service"
|
object_path = "/" + interface.replace(".", "/")
|
||||||
|
readerArguments = ["--reader", args.reader] if args.reader else []
|
||||||
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"/>'
|
||||||
@@ -107,7 +110,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
while not ready.exists() and app.poll() is None and time.monotonic() < deadline:
|
while not ready.exists() and app.poll() is None and time.monotonic() < deadline:
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
assert ready.exists(), "editor did not become ready before helper attachment"
|
assert ready.exists(), "editor did not become ready before helper attachment"
|
||||||
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller"], env=env)
|
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller", *readerArguments], env=env)
|
||||||
# Allow the helper to register its bridge before opening the fixture.
|
# Allow the helper to register its bridge before opening the fixture.
|
||||||
deadline = time.monotonic() + 1
|
deadline = time.monotonic() + 1
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
@@ -125,7 +128,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
if args.restart_helper and not restarted and any(e[0].endswith(":text") for e in events):
|
if args.restart_helper and not restarted and any(e[0].endswith(":text") for e in events):
|
||||||
helper.terminate()
|
helper.terminate()
|
||||||
helper.wait(timeout=5)
|
helper.wait(timeout=5)
|
||||||
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller"], env=env)
|
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller", *readerArguments], env=env)
|
||||||
restarted = True
|
restarted = True
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
assert app.poll() == 0, "fixture failed or timed out"
|
assert app.poll() == 0, "fixture failed or timed out"
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ parser = argparse.ArgumentParser()
|
|||||||
parser.add_argument('--build-dir', default='build')
|
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'))
|
||||||
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), *(['--32-bit'] if args.is32 else [])]))
|
'--session', '--build-dir', str(build),
|
||||||
iface = 'org.stormux.Cthulhu1.Service'
|
*(['--reader', args.reader] if args.reader else []),
|
||||||
path = '/org/stormux/Cthulhu1/Service'
|
*(['--32-bit'] if args.is32 else [])]))
|
||||||
|
iface = 'org.gnome.Orca1.Service' if args.reader == 'orca' else 'org.stormux.Cthulhu1.Service'
|
||||||
|
path = '/' + iface.replace('.', '/')
|
||||||
|
readerArguments = ['--reader', args.reader] if args.reader else []
|
||||||
capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
||||||
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
return result[0]
|
return result[0]
|
||||||
try:
|
try:
|
||||||
subprocess.run(['wineboot','-u'],env=env,check=True,timeout=45)
|
subprocess.run(['wineboot','-u'],env=env,check=True,timeout=45)
|
||||||
helper = subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller'],env=env)
|
helper = subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller',*readerArguments],env=env)
|
||||||
app = subprocess.Popen(['wine',str(fixture)],env=env)
|
app = subprocess.Popen(['wine',str(fixture)],env=env)
|
||||||
pump_until(lambda: any(f[4]=='Wine review fixture' for f in focuses.values()),8)
|
pump_until(lambda: any(f[4]=='Wine review fixture' for f in focuses.values()),8)
|
||||||
reply, elapsed = capture()
|
reply, elapsed = capture()
|
||||||
@@ -110,11 +114,37 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
print('PASS: test_capture_wire_contract')
|
print('PASS: test_capture_wire_contract')
|
||||||
other = Gio.DBusConnection.new_for_address_sync(os.environ['DBUS_SESSION_BUS_ADDRESS'],
|
other = Gio.DBusConnection.new_for_address_sync(os.environ['DBUS_SESSION_BUS_ADDRESS'],
|
||||||
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT|Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,None,None)
|
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT|Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,None,None)
|
||||||
|
# Owning the unselected reader name must not authorize capture.
|
||||||
|
otherIface = 'org.stormux.Cthulhu1.Service' if args.reader == 'orca' else 'org.gnome.Orca1.Service'
|
||||||
|
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||||
|
GLib.Variant('(su)',(otherIface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
sender,focus = next((s,f) for s,f in focuses.items() if f[4]=='Wine review fixture')
|
sender,focus = next((s,f) for s,f in focuses.items() if f[4]=='Wine review fixture')
|
||||||
wrong = request(other,sender,focus); pump_until(lambda:bool(wrong),2)
|
wrong = request(other,sender,focus); pump_until(lambda:bool(wrong),2)
|
||||||
assert isinstance(wrong[0],GLib.Error) and 'AccessDenied' in str(wrong[0])
|
assert isinstance(wrong[0],GLib.Error) and 'AccessDenied' in str(wrong[0])
|
||||||
other.close_sync(None)
|
|
||||||
print('PASS: test_wrong_caller')
|
print('PASS: test_wrong_caller')
|
||||||
|
# Move the selected name to a new connection. The helper must register
|
||||||
|
# again, authorize the new owner, and immediately reject the old owner.
|
||||||
|
other.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0],record,None,None)
|
||||||
|
providers.clear(); focuses.clear()
|
||||||
|
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
||||||
|
GLib.Variant('(s)',(iface,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
|
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||||
|
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
|
pump_until(lambda:sender in providers and sender in focuses)
|
||||||
|
focus=focuses[sender]
|
||||||
|
wrong=request(connection,sender,focus); pump_until(lambda:bool(wrong),2)
|
||||||
|
assert isinstance(wrong[0],GLib.Error) and 'AccessDenied' in str(wrong[0])
|
||||||
|
accepted=request(other,sender,focus); pump_until(lambda:bool(accepted),2)
|
||||||
|
assert not isinstance(accepted[0],GLib.Error), accepted[0]
|
||||||
|
assert accepted[0][0].unpack()[0]['status']=='ok'
|
||||||
|
providers.clear(); focuses.clear()
|
||||||
|
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
||||||
|
GLib.Variant('(s)',(iface,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
|
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||||
|
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||||
|
pump_until(lambda:sender in providers and sender in focuses)
|
||||||
|
other.close_sync(None)
|
||||||
|
print('PASS: test_reader_owner_replacement')
|
||||||
configure(33)
|
configure(33)
|
||||||
reply,_ = capture(); data=reply.unpack()[0]
|
reply,_ = capture(); data=reply.unpack()[0]
|
||||||
assert data['status']=='ok', data['status']
|
assert data['status']=='ok', data['status']
|
||||||
@@ -152,7 +182,7 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
helper.terminate(); helper.wait(timeout=3)
|
helper.terminate(); helper.wait(timeout=3)
|
||||||
subprocess.run(['wine',str(fixture),'--check-remote'],env=env,check=True,timeout=5)
|
subprocess.run(['wine',str(fixture),'--check-remote'],env=env,check=True,timeout=5)
|
||||||
print('PASS: test_native_buffer_lifetime_after_helper_stop')
|
print('PASS: test_native_buffer_lifetime_after_helper_stop')
|
||||||
helper=subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller'],env=env)
|
helper=subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller',*readerArguments],env=env)
|
||||||
pump_until(lambda:any(s != sender and f[4]=='Wine review fixture' for s,f in focuses.items()),5)
|
pump_until(lambda:any(s != sender and f[4]=='Wine review fixture' for s,f in focuses.items()),5)
|
||||||
focuses.pop(sender,None)
|
focuses.pop(sender,None)
|
||||||
configure(7)
|
configure(7)
|
||||||
@@ -180,7 +210,7 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
connection.unregister_object(registration)
|
connection.unregister_object(registration)
|
||||||
registration = connection.register_object(path,Gio.DBusNodeInfo.new_for_xml(interface_xml(False)).interfaces[0],record,None,None)
|
registration = connection.register_object(path,Gio.DBusNodeInfo.new_for_xml(interface_xml(False)).interfaces[0],record,None,None)
|
||||||
events.clear()
|
events.clear()
|
||||||
helper = subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller'],env=env)
|
helper = subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller',*readerArguments],env=env)
|
||||||
end=time.monotonic()+1
|
end=time.monotonic()+1
|
||||||
while time.monotonic()<end:
|
while time.monotonic()<end:
|
||||||
while GLib.MainContext.default().iteration(False): pass
|
while GLib.MainContext.default().iteration(False): pass
|
||||||
|
|||||||
Reference in new issue
Block a user